logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
38
crates/text/Cargo.toml
Normal file
38
crates/text/Cargo.toml
Normal file
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "text"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/text.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["rand", "util/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
rand = { workspace = true, optional = true }
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
smallvec.workspace = true
|
||||
sum_tree.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
1
crates/text/LICENSE-GPL
Symbolic link
1
crates/text/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
249
crates/text/src/anchor.rs
Normal file
249
crates/text/src/anchor.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
use crate::{
|
||||
BufferId, BufferSnapshot, Point, PointUtf16, TextDimension, ToOffset, ToPoint, ToPointUtf16,
|
||||
locator::Locator,
|
||||
};
|
||||
use std::{cmp::Ordering, fmt::Debug, ops::Range};
|
||||
use sum_tree::{Bias, Dimensions};
|
||||
|
||||
/// A timestamped position in a buffer.
|
||||
#[doc(alias = "TextAnchor")]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Anchor {
|
||||
// /// The timestamp of the operation that inserted the text
|
||||
// /// in which this anchor is located.
|
||||
// pub(crate) timestamp: clock::Lamport,
|
||||
// we store the replica id and sequence number of the timestamp inline
|
||||
// to avoid the alignment of our fields from increasing the size of this struct
|
||||
// This saves 8 bytes, by allowing replica id, value and bias to occupy the padding
|
||||
pub(crate) timestamp_replica_id: clock::ReplicaId,
|
||||
pub(crate) timestamp_value: clock::Seq,
|
||||
|
||||
/// The byte offset into the text inserted in the operation
|
||||
/// at `timestamp`.
|
||||
pub offset: u32,
|
||||
/// Whether this anchor stays attached to the character *before* or *after*
|
||||
/// the offset.
|
||||
pub bias: Bias,
|
||||
pub buffer_id: BufferId,
|
||||
}
|
||||
|
||||
impl Debug for Anchor {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.is_min() {
|
||||
return write!(f, "Anchor::min({:?})", self.buffer_id);
|
||||
}
|
||||
if self.is_max() {
|
||||
return write!(f, "Anchor::max({:?})", self.buffer_id);
|
||||
}
|
||||
|
||||
f.debug_struct("Anchor")
|
||||
.field("timestamp", &self.timestamp())
|
||||
.field("offset", &self.offset)
|
||||
.field("bias", &self.bias)
|
||||
.field("buffer_id", &self.buffer_id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Anchor {
|
||||
pub fn new(timestamp: clock::Lamport, offset: u32, bias: Bias, buffer_id: BufferId) -> Self {
|
||||
Self {
|
||||
timestamp_replica_id: timestamp.replica_id,
|
||||
timestamp_value: timestamp.value,
|
||||
offset,
|
||||
bias,
|
||||
buffer_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn min_for_buffer(buffer_id: BufferId) -> Self {
|
||||
Self {
|
||||
timestamp_replica_id: clock::Lamport::MIN.replica_id,
|
||||
timestamp_value: clock::Lamport::MIN.value,
|
||||
offset: u32::MIN,
|
||||
bias: Bias::Left,
|
||||
buffer_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_for_buffer(buffer_id: BufferId) -> Self {
|
||||
Self {
|
||||
timestamp_replica_id: clock::Lamport::MAX.replica_id,
|
||||
timestamp_value: clock::Lamport::MAX.value,
|
||||
offset: u32::MAX,
|
||||
bias: Bias::Right,
|
||||
buffer_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn min_min_range_for_buffer(buffer_id: BufferId) -> std::ops::Range<Self> {
|
||||
let min = Self::min_for_buffer(buffer_id);
|
||||
min..min
|
||||
}
|
||||
pub fn max_max_range_for_buffer(buffer_id: BufferId) -> std::ops::Range<Self> {
|
||||
let max = Self::max_for_buffer(buffer_id);
|
||||
max..max
|
||||
}
|
||||
pub fn min_max_range_for_buffer(buffer_id: BufferId) -> std::ops::Range<Self> {
|
||||
Self::min_for_buffer(buffer_id)..Self::max_for_buffer(buffer_id)
|
||||
}
|
||||
|
||||
pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering {
|
||||
let fragment_id_comparison = if self.timestamp() == other.timestamp() {
|
||||
Ordering::Equal
|
||||
} else {
|
||||
buffer
|
||||
.fragment_id_for_anchor(self)
|
||||
.cmp(buffer.fragment_id_for_anchor(other))
|
||||
};
|
||||
|
||||
fragment_id_comparison
|
||||
.then_with(|| self.offset.cmp(&other.offset))
|
||||
.then_with(|| self.bias.cmp(&other.bias))
|
||||
}
|
||||
|
||||
pub fn min<'a>(&'a self, other: &'a Self, buffer: &BufferSnapshot) -> &'a Self {
|
||||
if self.cmp(other, buffer).is_le() {
|
||||
self
|
||||
} else {
|
||||
other
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max<'a>(&'a self, other: &'a Self, buffer: &BufferSnapshot) -> &'a Self {
|
||||
if self.cmp(other, buffer).is_ge() {
|
||||
self
|
||||
} else {
|
||||
other
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bias(&self, bias: Bias, buffer: &BufferSnapshot) -> Anchor {
|
||||
match bias {
|
||||
Bias::Left => self.bias_left(buffer),
|
||||
Bias::Right => self.bias_right(buffer),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bias_left(&self, buffer: &BufferSnapshot) -> Anchor {
|
||||
match self.bias {
|
||||
Bias::Left => *self,
|
||||
Bias::Right => buffer.anchor_before(self),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bias_right(&self, buffer: &BufferSnapshot) -> Anchor {
|
||||
match self.bias {
|
||||
Bias::Left => buffer.anchor_after(self),
|
||||
Bias::Right => *self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summary<D>(&self, content: &BufferSnapshot) -> D
|
||||
where
|
||||
D: TextDimension,
|
||||
{
|
||||
content.summary_for_anchor(self)
|
||||
}
|
||||
|
||||
/// Returns true when the [`Anchor`] is located inside a visible fragment.
|
||||
pub fn is_valid(&self, buffer: &BufferSnapshot) -> bool {
|
||||
if self.is_min() || self.is_max() {
|
||||
true
|
||||
} else if self.buffer_id != buffer.remote_id {
|
||||
false
|
||||
} else {
|
||||
let Some(fragment_id) = buffer.try_fragment_id_for_anchor(self) else {
|
||||
return false;
|
||||
};
|
||||
let (.., item) = buffer
|
||||
.fragments
|
||||
.find::<Dimensions<Option<&Locator>, usize>, _>(
|
||||
&None,
|
||||
&Some(fragment_id),
|
||||
Bias::Left,
|
||||
);
|
||||
item.is_some_and(|fragment| fragment.visible)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_min(&self) -> bool {
|
||||
self.timestamp() == clock::Lamport::MIN
|
||||
&& self.offset == u32::MIN
|
||||
&& self.bias == Bias::Left
|
||||
}
|
||||
|
||||
pub fn is_max(&self) -> bool {
|
||||
self.timestamp() == clock::Lamport::MAX
|
||||
&& self.offset == u32::MAX
|
||||
&& self.bias == Bias::Right
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn timestamp(&self) -> clock::Lamport {
|
||||
clock::Lamport {
|
||||
replica_id: self.timestamp_replica_id,
|
||||
value: self.timestamp_value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn opaque_id(&self) -> [u8; 20] {
|
||||
let mut bytes = [0u8; 20];
|
||||
let buffer_id: u64 = self.buffer_id.into();
|
||||
bytes[0..8].copy_from_slice(&buffer_id.to_le_bytes());
|
||||
bytes[8..12].copy_from_slice(&self.offset.to_le_bytes());
|
||||
bytes[12..16].copy_from_slice(&self.timestamp_value.to_le_bytes());
|
||||
let replica_id = self.timestamp_replica_id.as_u16();
|
||||
bytes[16..18].copy_from_slice(&replica_id.to_le_bytes());
|
||||
bytes[18] = self.bias as u8;
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OffsetRangeExt {
|
||||
fn to_offset(&self, snapshot: &BufferSnapshot) -> Range<usize>;
|
||||
fn to_point(&self, snapshot: &BufferSnapshot) -> Range<Point>;
|
||||
fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range<PointUtf16>;
|
||||
}
|
||||
|
||||
impl<T> OffsetRangeExt for Range<T>
|
||||
where
|
||||
T: ToOffset,
|
||||
{
|
||||
fn to_offset(&self, snapshot: &BufferSnapshot) -> Range<usize> {
|
||||
self.start.to_offset(snapshot)..self.end.to_offset(snapshot)
|
||||
}
|
||||
|
||||
fn to_point(&self, snapshot: &BufferSnapshot) -> Range<Point> {
|
||||
self.start.to_offset(snapshot).to_point(snapshot)
|
||||
..self.end.to_offset(snapshot).to_point(snapshot)
|
||||
}
|
||||
|
||||
fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range<PointUtf16> {
|
||||
self.start.to_offset(snapshot).to_point_utf16(snapshot)
|
||||
..self.end.to_offset(snapshot).to_point_utf16(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnchorRangeExt {
|
||||
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering;
|
||||
fn overlaps(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> bool;
|
||||
fn contains_anchor(&self, b: Anchor, buffer: &BufferSnapshot) -> bool;
|
||||
}
|
||||
|
||||
impl AnchorRangeExt for Range<Anchor> {
|
||||
fn cmp(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering {
|
||||
match self.start.cmp(&other.start, buffer) {
|
||||
Ordering::Equal => other.end.cmp(&self.end, buffer),
|
||||
ord => ord,
|
||||
}
|
||||
}
|
||||
|
||||
fn overlaps(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> bool {
|
||||
self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt()
|
||||
}
|
||||
|
||||
fn contains_anchor(&self, other: Anchor, buffer: &BufferSnapshot) -> bool {
|
||||
self.start.cmp(&other, buffer).is_le() && self.end.cmp(&other, buffer).is_ge()
|
||||
}
|
||||
}
|
||||
177
crates/text/src/locator.rs
Normal file
177
crates/text/src/locator.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
use smallvec::SmallVec;
|
||||
use std::iter;
|
||||
|
||||
/// An identifier for a position in a ordered collection.
|
||||
///
|
||||
/// Allows prepending and appending without needing to renumber existing locators
|
||||
/// using `Locator::between(lhs, rhs)`.
|
||||
///
|
||||
/// The initial location for a collection should be `Locator::between(Locator::min(), Locator::max())`,
|
||||
/// leaving room for items to be inserted before and after it.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Locator(SmallVec<[u64; 2]>);
|
||||
|
||||
impl Clone for Locator {
|
||||
fn clone(&self) -> Self {
|
||||
// We manually implement clone to avoid the overhead of SmallVec's clone implementation.
|
||||
// Using `from_slice` is faster than `clone` for SmallVec as we can use our `Copy` implementation of u64.
|
||||
Self {
|
||||
0: SmallVec::from_slice(&self.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_from(&mut self, source: &Self) {
|
||||
self.0.clone_from(&source.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Locator {
|
||||
pub const fn min() -> Self {
|
||||
// SAFETY: 1 is <= 2
|
||||
Self(unsafe { SmallVec::from_const_with_len_unchecked([u64::MIN; 2], 1) })
|
||||
}
|
||||
|
||||
pub const fn max() -> Self {
|
||||
// SAFETY: 1 is <= 2
|
||||
Self(unsafe { SmallVec::from_const_with_len_unchecked([u64::MAX; 2], 1) })
|
||||
}
|
||||
|
||||
pub const fn min_ref() -> &'static Self {
|
||||
const { &Self::min() }
|
||||
}
|
||||
|
||||
pub const fn max_ref() -> &'static Self {
|
||||
const { &Self::max() }
|
||||
}
|
||||
|
||||
pub fn assign(&mut self, other: &Self) {
|
||||
self.0.resize(other.0.len(), 0);
|
||||
self.0.copy_from_slice(&other.0);
|
||||
}
|
||||
|
||||
pub fn between(lhs: &Self, rhs: &Self) -> Self {
|
||||
let lhs = lhs.0.iter().copied().chain(iter::repeat(u64::MIN));
|
||||
let rhs = rhs.0.iter().copied().chain(iter::repeat(u64::MAX));
|
||||
let mut location = SmallVec::new();
|
||||
for (lhs, rhs) in lhs.zip(rhs) {
|
||||
// This shift is essential! It optimizes for the common case of sequential typing.
|
||||
let mid = lhs + ((rhs.saturating_sub(lhs)) >> 48);
|
||||
location.push(mid);
|
||||
if mid > lhs {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Self(location)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Locator {
|
||||
fn default() -> Self {
|
||||
Self::min()
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for Locator {
|
||||
type Summary = Locator;
|
||||
|
||||
fn summary(&self, _cx: ()) -> Self::Summary {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::KeyedItem for Locator {
|
||||
type Key = Locator;
|
||||
|
||||
fn key(&self) -> Self::Key {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::ContextLessSummary for Locator {
|
||||
fn zero() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &Self) {
|
||||
self.assign(summary);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
use std::mem;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_locators(mut rng: StdRng) {
|
||||
let mut lhs = Default::default();
|
||||
let mut rhs = Default::default();
|
||||
while lhs == rhs {
|
||||
lhs = Locator(
|
||||
(0..rng.random_range(1..=5))
|
||||
.map(|_| rng.random_range(0..=100))
|
||||
.collect(),
|
||||
);
|
||||
rhs = Locator(
|
||||
(0..rng.random_range(1..=5))
|
||||
.map(|_| rng.random_range(0..=100))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
if lhs > rhs {
|
||||
mem::swap(&mut lhs, &mut rhs);
|
||||
}
|
||||
|
||||
let middle = Locator::between(&lhs, &rhs);
|
||||
assert!(middle > lhs);
|
||||
assert!(middle < rhs);
|
||||
for ix in 0..middle.0.len() - 1 {
|
||||
assert!(
|
||||
middle.0[ix] == *lhs.0.get(ix).unwrap_or(&0)
|
||||
|| middle.0[ix] == *rhs.0.get(ix).unwrap_or(&0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Simulates 100,000 sequential forward appends (the pattern used when
|
||||
// building a buffer's initial fragments and when
|
||||
// `push_fragments_for_insertion` chains new text fragments).
|
||||
#[test]
|
||||
fn test_sequential_forward_append_stays_at_depth_1() {
|
||||
let mut prev = Locator::min();
|
||||
let max = Locator::max();
|
||||
for _ in 0..100_000 {
|
||||
let loc = Locator::between(&prev, &max);
|
||||
assert_eq!(loc.len(), 1, "sequential forward append grew past depth 1");
|
||||
prev = loc;
|
||||
}
|
||||
}
|
||||
|
||||
// Simulates the most common real editing pattern: a fragment is split
|
||||
// (producing a depth-2 prefix), then 10,000 new fragments are inserted
|
||||
// sequentially forward within that split region.
|
||||
#[test]
|
||||
fn test_typing_at_cursor_stays_at_depth_2() {
|
||||
let initial = Locator::between(&Locator::min(), &Locator::max());
|
||||
let prefix = Locator::between(&Locator::min(), &initial);
|
||||
assert_eq!(prefix.len(), 2);
|
||||
|
||||
let suffix_id = initial;
|
||||
let mut prev = prefix;
|
||||
for _ in 0..10_000 {
|
||||
let loc = Locator::between(&prev, &suffix_id);
|
||||
assert_eq!(loc.len(), 2, "forward typing after split grew past depth 2");
|
||||
prev = loc;
|
||||
}
|
||||
}
|
||||
}
|
||||
94
crates/text/src/network.rs
Normal file
94
crates/text/src/network.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, HashSet};
|
||||
|
||||
pub struct Network<T: Clone, R: rand::Rng> {
|
||||
inboxes: BTreeMap<ReplicaId, Vec<Envelope<T>>>,
|
||||
disconnected_peers: HashSet<ReplicaId>,
|
||||
rng: R,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Envelope<T: Clone> {
|
||||
message: T,
|
||||
}
|
||||
|
||||
impl<T: Clone, R: rand::Rng> Network<T, R> {
|
||||
pub fn new(rng: R) -> Self {
|
||||
Network {
|
||||
inboxes: BTreeMap::default(),
|
||||
disconnected_peers: HashSet::default(),
|
||||
rng,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_peer(&mut self, id: ReplicaId) {
|
||||
self.inboxes.insert(id, Vec::new());
|
||||
}
|
||||
|
||||
pub fn disconnect_peer(&mut self, id: ReplicaId) {
|
||||
self.disconnected_peers.insert(id);
|
||||
self.inboxes.get_mut(&id).unwrap().clear();
|
||||
}
|
||||
|
||||
pub fn reconnect_peer(&mut self, id: ReplicaId, replicate_from: ReplicaId) {
|
||||
assert!(self.disconnected_peers.remove(&id));
|
||||
self.replicate(replicate_from, id);
|
||||
}
|
||||
|
||||
pub fn is_disconnected(&self, id: ReplicaId) -> bool {
|
||||
self.disconnected_peers.contains(&id)
|
||||
}
|
||||
|
||||
pub fn contains_disconnected_peers(&self) -> bool {
|
||||
!self.disconnected_peers.is_empty()
|
||||
}
|
||||
|
||||
pub fn replicate(&mut self, old_replica_id: ReplicaId, new_replica_id: ReplicaId) {
|
||||
self.inboxes
|
||||
.insert(new_replica_id, self.inboxes[&old_replica_id].clone());
|
||||
}
|
||||
|
||||
pub fn is_idle(&self) -> bool {
|
||||
self.inboxes.values().all(|i| i.is_empty())
|
||||
}
|
||||
|
||||
pub fn broadcast(&mut self, sender: ReplicaId, messages: Vec<T>) {
|
||||
// Drop messages from disconnected peers.
|
||||
if self.disconnected_peers.contains(&sender) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (replica, inbox) in self.inboxes.iter_mut() {
|
||||
if *replica != sender && !self.disconnected_peers.contains(replica) {
|
||||
for message in &messages {
|
||||
// Insert one or more duplicates of this message, potentially *before* the previous
|
||||
// message sent by this peer to simulate out-of-order delivery.
|
||||
for _ in 0..self.rng.random_range(1..4) {
|
||||
let insertion_index = self.rng.random_range(0..inbox.len() + 1);
|
||||
inbox.insert(
|
||||
insertion_index,
|
||||
Envelope {
|
||||
message: message.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_unreceived(&self, receiver: ReplicaId) -> bool {
|
||||
!self.inboxes[&receiver].is_empty()
|
||||
}
|
||||
|
||||
pub fn receive(&mut self, receiver: ReplicaId) -> Vec<T> {
|
||||
let inbox = self.inboxes.get_mut(&receiver).unwrap();
|
||||
let count = self.rng.random_range(0..inbox.len() + 1);
|
||||
inbox
|
||||
.drain(0..count)
|
||||
.map(|envelope| envelope.message)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
165
crates/text/src/operation_queue.rs
Normal file
165
crates/text/src/operation_queue.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use clock::Lamport;
|
||||
use std::{fmt::Debug, ops::Add};
|
||||
use sum_tree::{ContextLessSummary, Dimension, Edit, Item, KeyedItem, SumTree};
|
||||
|
||||
pub trait Operation: Clone + Debug {
|
||||
fn lamport_timestamp(&self) -> clock::Lamport;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct OperationItem<T>(T);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OperationQueue<T: Operation>(SumTree<OperationItem<T>>);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct OperationKey(clock::Lamport);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct OperationSummary {
|
||||
pub key: OperationKey,
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
impl OperationKey {
|
||||
pub fn new(timestamp: clock::Lamport) -> Self {
|
||||
Self(timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Operation> Default for OperationQueue<T> {
|
||||
fn default() -> Self {
|
||||
OperationQueue::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Operation> OperationQueue<T> {
|
||||
pub fn new() -> Self {
|
||||
OperationQueue(SumTree::default())
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.summary().len
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, mut ops: Vec<T>) {
|
||||
ops.sort_by_key(|op| op.lamport_timestamp());
|
||||
ops.dedup_by_key(|op| op.lamport_timestamp());
|
||||
self.0.edit(
|
||||
ops.into_iter()
|
||||
.map(|op| Edit::Insert(OperationItem(op)))
|
||||
.collect(),
|
||||
(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn drain(&mut self) -> Self {
|
||||
let clone = self.clone();
|
||||
self.0 = SumTree::default();
|
||||
clone
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
self.0.iter().map(|i| &i.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextLessSummary for OperationSummary {
|
||||
fn zero() -> Self {
|
||||
OperationSummary {
|
||||
key: OperationKey::new(Lamport::MIN),
|
||||
len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, other: &Self) {
|
||||
assert!(self.key < other.key);
|
||||
self.key = other.key;
|
||||
self.len += other.len;
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<&Self> for OperationSummary {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, other: &Self) -> Self {
|
||||
assert!(self.key < other.key);
|
||||
OperationSummary {
|
||||
key: other.key,
|
||||
len: self.len + other.len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dimension<'_, OperationSummary> for OperationKey {
|
||||
fn zero(_cx: ()) -> Self {
|
||||
OperationKey::new(Lamport::MIN)
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &OperationSummary, _: ()) {
|
||||
assert!(*self <= summary.key);
|
||||
*self = summary.key;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Operation> Item for OperationItem<T> {
|
||||
type Summary = OperationSummary;
|
||||
|
||||
fn summary(&self, _cx: ()) -> Self::Summary {
|
||||
OperationSummary {
|
||||
key: OperationKey::new(self.0.lamport_timestamp()),
|
||||
len: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Operation> KeyedItem for OperationItem<T> {
|
||||
type Key = OperationKey;
|
||||
|
||||
fn key(&self) -> Self::Key {
|
||||
OperationKey::new(self.0.lamport_timestamp())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clock::ReplicaId;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_len() {
|
||||
let mut clock = clock::Lamport::new(ReplicaId::LOCAL);
|
||||
|
||||
let mut queue = OperationQueue::new();
|
||||
assert_eq!(queue.len(), 0);
|
||||
|
||||
queue.insert(vec![
|
||||
TestOperation(clock.tick()),
|
||||
TestOperation(clock.tick()),
|
||||
]);
|
||||
assert_eq!(queue.len(), 2);
|
||||
|
||||
queue.insert(vec![TestOperation(clock.tick())]);
|
||||
assert_eq!(queue.len(), 3);
|
||||
|
||||
drop(queue.drain());
|
||||
assert_eq!(queue.len(), 0);
|
||||
|
||||
queue.insert(vec![TestOperation(clock.tick())]);
|
||||
assert_eq!(queue.len(), 1);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct TestOperation(clock::Lamport);
|
||||
|
||||
impl Operation for TestOperation {
|
||||
fn lamport_timestamp(&self) -> clock::Lamport {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
}
|
||||
655
crates/text/src/patch.rs
Normal file
655
crates/text/src/patch.rs
Normal file
@@ -0,0 +1,655 @@
|
||||
use crate::Edit;
|
||||
use std::{
|
||||
cmp, mem,
|
||||
ops::{Add, AddAssign, Sub},
|
||||
};
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, Eq)]
|
||||
pub struct Patch<T>(Vec<Edit<T>>);
|
||||
|
||||
impl<T> Patch<T>
|
||||
where
|
||||
T: 'static + Clone + Copy + Ord + Default,
|
||||
{
|
||||
pub const fn empty() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
|
||||
pub fn new(edits: Vec<Edit<T>>) -> Self {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let mut last_edit: Option<&Edit<T>> = None;
|
||||
for edit in &edits {
|
||||
if let Some(last_edit) = last_edit {
|
||||
assert!(edit.old.start > last_edit.old.end);
|
||||
assert!(edit.new.start > last_edit.new.end);
|
||||
}
|
||||
last_edit = Some(edit);
|
||||
}
|
||||
}
|
||||
Self(edits)
|
||||
}
|
||||
|
||||
pub fn edits(&self) -> &[Edit<T>] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> Vec<Edit<T>> {
|
||||
self.0
|
||||
}
|
||||
pub fn invert(&mut self) -> &mut Self {
|
||||
for edit in &mut self.0 {
|
||||
mem::swap(&mut edit.old, &mut edit.new);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear();
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn push(&mut self, edit: Edit<T>) {
|
||||
if edit.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.push_maybe_empty(edit);
|
||||
}
|
||||
|
||||
pub fn push_maybe_empty(&mut self, edit: Edit<T>) {
|
||||
if let Some(last) = self.0.last_mut() {
|
||||
if last.old.end >= edit.old.start {
|
||||
last.old.end = edit.old.end;
|
||||
last.new.end = edit.new.end;
|
||||
} else {
|
||||
self.0.push(edit);
|
||||
}
|
||||
} else {
|
||||
self.0.push(edit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, TDelta> Patch<T>
|
||||
where
|
||||
T: 'static
|
||||
+ Copy
|
||||
+ Ord
|
||||
+ Sub<T, Output = TDelta>
|
||||
+ Add<TDelta, Output = T>
|
||||
+ AddAssign<TDelta>
|
||||
+ Default,
|
||||
TDelta: Ord + Copy,
|
||||
{
|
||||
#[must_use]
|
||||
pub fn compose(&self, new_edits_iter: impl IntoIterator<Item = Edit<T>>) -> Self {
|
||||
let mut old_edits_iter = self.0.iter().cloned().peekable();
|
||||
let mut new_edits_iter = new_edits_iter.into_iter().peekable();
|
||||
let mut composed = Patch(Vec::new());
|
||||
|
||||
let mut old_start = T::default();
|
||||
let mut new_start = T::default();
|
||||
loop {
|
||||
let old_edit = old_edits_iter.peek_mut();
|
||||
let new_edit = new_edits_iter.peek_mut();
|
||||
|
||||
// Push the old edit if its new end is before the new edit's old start.
|
||||
if let Some(old_edit) = old_edit.as_ref() {
|
||||
let new_edit = new_edit.as_ref();
|
||||
if new_edit.is_none_or(|new_edit| old_edit.new.end < new_edit.old.start) {
|
||||
let catchup = old_edit.old.start - old_start;
|
||||
old_start += catchup;
|
||||
new_start += catchup;
|
||||
|
||||
let old_end = old_start + old_edit.old_len();
|
||||
let new_end = new_start + old_edit.new_len();
|
||||
composed.push(Edit {
|
||||
old: old_start..old_end,
|
||||
new: new_start..new_end,
|
||||
});
|
||||
old_start = old_end;
|
||||
new_start = new_end;
|
||||
old_edits_iter.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Push the new edit if its old end is before the old edit's new start.
|
||||
if let Some(new_edit) = new_edit.as_ref() {
|
||||
let old_edit = old_edit.as_ref();
|
||||
if old_edit.is_none_or(|old_edit| new_edit.old.end < old_edit.new.start) {
|
||||
let catchup = new_edit.new.start - new_start;
|
||||
old_start += catchup;
|
||||
new_start += catchup;
|
||||
|
||||
let old_end = old_start + new_edit.old_len();
|
||||
let new_end = new_start + new_edit.new_len();
|
||||
composed.push(Edit {
|
||||
old: old_start..old_end,
|
||||
new: new_start..new_end,
|
||||
});
|
||||
old_start = old_end;
|
||||
new_start = new_end;
|
||||
new_edits_iter.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we still have edits by this point then they must intersect, so we compose them.
|
||||
if let Some((old_edit, new_edit)) = old_edit.zip(new_edit) {
|
||||
if old_edit.new.start < new_edit.old.start {
|
||||
let catchup = old_edit.old.start - old_start;
|
||||
old_start += catchup;
|
||||
new_start += catchup;
|
||||
|
||||
let overshoot = new_edit.old.start - old_edit.new.start;
|
||||
let old_end = cmp::min(old_start + overshoot, old_edit.old.end);
|
||||
let new_end = new_start + overshoot;
|
||||
composed.push(Edit {
|
||||
old: old_start..old_end,
|
||||
new: new_start..new_end,
|
||||
});
|
||||
|
||||
old_edit.old.start = old_end;
|
||||
old_edit.new.start += overshoot;
|
||||
old_start = old_end;
|
||||
new_start = new_end;
|
||||
} else {
|
||||
let catchup = new_edit.new.start - new_start;
|
||||
old_start += catchup;
|
||||
new_start += catchup;
|
||||
|
||||
let overshoot = old_edit.new.start - new_edit.old.start;
|
||||
let old_end = old_start + overshoot;
|
||||
let new_end = cmp::min(new_start + overshoot, new_edit.new.end);
|
||||
composed.push(Edit {
|
||||
old: old_start..old_end,
|
||||
new: new_start..new_end,
|
||||
});
|
||||
|
||||
new_edit.old.start += overshoot;
|
||||
new_edit.new.start = new_end;
|
||||
old_start = old_end;
|
||||
new_start = new_end;
|
||||
}
|
||||
|
||||
if old_edit.new.end > new_edit.old.end {
|
||||
let old_end = old_start + cmp::min(old_edit.old_len(), new_edit.old_len());
|
||||
let new_end = new_start + new_edit.new_len();
|
||||
composed.push(Edit {
|
||||
old: old_start..old_end,
|
||||
new: new_start..new_end,
|
||||
});
|
||||
|
||||
old_edit.old.start = old_end;
|
||||
old_edit.new.start = new_edit.old.end;
|
||||
old_start = old_end;
|
||||
new_start = new_end;
|
||||
new_edits_iter.next();
|
||||
} else {
|
||||
let old_end = old_start + old_edit.old_len();
|
||||
let new_end = new_start + cmp::min(old_edit.new_len(), new_edit.new_len());
|
||||
composed.push(Edit {
|
||||
old: old_start..old_end,
|
||||
new: new_start..new_end,
|
||||
});
|
||||
|
||||
new_edit.old.start = old_edit.new.end;
|
||||
new_edit.new.start = new_end;
|
||||
old_start = old_end;
|
||||
new_start = new_end;
|
||||
old_edits_iter.next();
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
composed
|
||||
}
|
||||
|
||||
pub fn old_to_new(&self, old: T) -> T {
|
||||
let ix = match self.0.binary_search_by(|probe| probe.old.start.cmp(&old)) {
|
||||
Ok(ix) => ix,
|
||||
Err(ix) => {
|
||||
if ix == 0 {
|
||||
return old;
|
||||
} else {
|
||||
ix - 1
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(edit) = self.0.get(ix) {
|
||||
if old >= edit.old.end {
|
||||
edit.new.end + (old - edit.old.end)
|
||||
} else {
|
||||
edit.new.start
|
||||
}
|
||||
} else {
|
||||
old
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the edit that touches the given old position.
|
||||
///
|
||||
/// An edit is considered to touch the given old position if edit.old.start <= old <= edit.old.end (note, inclusive on the right).
|
||||
///
|
||||
/// If there are no edits touching the given old position, an empty edit with appropriate (empty) old and new ranges is returned.
|
||||
pub fn edit_for_old_position(&self, old: T) -> Edit<T> {
|
||||
let edits = self.edits();
|
||||
|
||||
let ix = match edits.binary_search_by(|probe| probe.old.start.cmp(&old)) {
|
||||
Ok(ix) => ix,
|
||||
Err(ix) => {
|
||||
if ix == 0 {
|
||||
return Edit {
|
||||
old: old..old,
|
||||
new: old..old,
|
||||
};
|
||||
} else {
|
||||
ix - 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(edit) = edits.get(ix) {
|
||||
if old > edit.old.end {
|
||||
let translated = edit.new.end + (old - edit.old.end);
|
||||
Edit {
|
||||
new: translated..translated,
|
||||
old: old..old,
|
||||
}
|
||||
} else {
|
||||
edit.clone()
|
||||
}
|
||||
} else {
|
||||
Edit {
|
||||
old: old..old,
|
||||
new: old..old,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Patch<T> {
|
||||
pub fn retain_mut<F>(&mut self, f: F)
|
||||
where
|
||||
F: FnMut(&mut Edit<T>) -> bool,
|
||||
{
|
||||
self.0.retain_mut(f);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> IntoIterator for Patch<T> {
|
||||
type Item = Edit<T>;
|
||||
type IntoIter = std::vec::IntoIter<Edit<T>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Clone> IntoIterator for &'a Patch<T> {
|
||||
type Item = Edit<T>;
|
||||
type IntoIter = std::iter::Cloned<std::slice::Iter<'a, Edit<T>>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter().cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Clone> IntoIterator for &'a mut Patch<T> {
|
||||
type Item = Edit<T>;
|
||||
type IntoIter = std::iter::Cloned<std::slice::Iter<'a, Edit<T>>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter().cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
use std::env;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_one_disjoint_edit() {
|
||||
assert_patch_composition(
|
||||
Patch(vec![Edit {
|
||||
old: 1..3,
|
||||
new: 1..4,
|
||||
}]),
|
||||
Patch(vec![Edit {
|
||||
old: 0..0,
|
||||
new: 0..4,
|
||||
}]),
|
||||
Patch(vec![
|
||||
Edit {
|
||||
old: 0..0,
|
||||
new: 0..4,
|
||||
},
|
||||
Edit {
|
||||
old: 1..3,
|
||||
new: 5..8,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
assert_patch_composition(
|
||||
Patch(vec![Edit {
|
||||
old: 1..3,
|
||||
new: 1..4,
|
||||
}]),
|
||||
Patch(vec![Edit {
|
||||
old: 5..9,
|
||||
new: 5..7,
|
||||
}]),
|
||||
Patch(vec![
|
||||
Edit {
|
||||
old: 1..3,
|
||||
new: 1..4,
|
||||
},
|
||||
Edit {
|
||||
old: 4..8,
|
||||
new: 5..7,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_one_overlapping_edit() {
|
||||
assert_patch_composition(
|
||||
Patch(vec![Edit {
|
||||
old: 1..3,
|
||||
new: 1..4,
|
||||
}]),
|
||||
Patch(vec![Edit {
|
||||
old: 3..5,
|
||||
new: 3..6,
|
||||
}]),
|
||||
Patch(vec![Edit {
|
||||
old: 1..4,
|
||||
new: 1..6,
|
||||
}]),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_two_disjoint_and_overlapping() {
|
||||
assert_patch_composition(
|
||||
Patch(vec![
|
||||
Edit {
|
||||
old: 1..3,
|
||||
new: 1..4,
|
||||
},
|
||||
Edit {
|
||||
old: 8..12,
|
||||
new: 9..11,
|
||||
},
|
||||
]),
|
||||
Patch(vec![
|
||||
Edit {
|
||||
old: 0..0,
|
||||
new: 0..4,
|
||||
},
|
||||
Edit {
|
||||
old: 3..10,
|
||||
new: 7..9,
|
||||
},
|
||||
]),
|
||||
Patch(vec![
|
||||
Edit {
|
||||
old: 0..0,
|
||||
new: 0..4,
|
||||
},
|
||||
Edit {
|
||||
old: 1..12,
|
||||
new: 5..10,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_two_new_edits_overlapping_one_old_edit() {
|
||||
assert_patch_composition(
|
||||
Patch(vec![Edit {
|
||||
old: 0..0,
|
||||
new: 0..3,
|
||||
}]),
|
||||
Patch(vec![
|
||||
Edit {
|
||||
old: 0..0,
|
||||
new: 0..1,
|
||||
},
|
||||
Edit {
|
||||
old: 1..2,
|
||||
new: 2..2,
|
||||
},
|
||||
]),
|
||||
Patch(vec![Edit {
|
||||
old: 0..0,
|
||||
new: 0..3,
|
||||
}]),
|
||||
);
|
||||
|
||||
assert_patch_composition(
|
||||
Patch(vec![Edit {
|
||||
old: 2..3,
|
||||
new: 2..4,
|
||||
}]),
|
||||
Patch(vec![
|
||||
Edit {
|
||||
old: 0..2,
|
||||
new: 0..1,
|
||||
},
|
||||
Edit {
|
||||
old: 3..3,
|
||||
new: 2..5,
|
||||
},
|
||||
]),
|
||||
Patch(vec![Edit {
|
||||
old: 0..3,
|
||||
new: 0..6,
|
||||
}]),
|
||||
);
|
||||
|
||||
assert_patch_composition(
|
||||
Patch(vec![Edit {
|
||||
old: 0..0,
|
||||
new: 0..2,
|
||||
}]),
|
||||
Patch(vec![
|
||||
Edit {
|
||||
old: 0..0,
|
||||
new: 0..2,
|
||||
},
|
||||
Edit {
|
||||
old: 2..5,
|
||||
new: 4..4,
|
||||
},
|
||||
]),
|
||||
Patch(vec![Edit {
|
||||
old: 0..3,
|
||||
new: 0..4,
|
||||
}]),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_two_new_edits_touching_one_old_edit() {
|
||||
assert_patch_composition(
|
||||
Patch(vec![
|
||||
Edit {
|
||||
old: 2..3,
|
||||
new: 2..4,
|
||||
},
|
||||
Edit {
|
||||
old: 7..7,
|
||||
new: 8..11,
|
||||
},
|
||||
]),
|
||||
Patch(vec![
|
||||
Edit {
|
||||
old: 2..3,
|
||||
new: 2..2,
|
||||
},
|
||||
Edit {
|
||||
old: 4..4,
|
||||
new: 3..4,
|
||||
},
|
||||
]),
|
||||
Patch(vec![
|
||||
Edit {
|
||||
old: 2..3,
|
||||
new: 2..4,
|
||||
},
|
||||
Edit {
|
||||
old: 7..7,
|
||||
new: 8..11,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_old_to_new() {
|
||||
let patch = Patch(vec![
|
||||
Edit {
|
||||
old: 2..4,
|
||||
new: 2..4,
|
||||
},
|
||||
Edit {
|
||||
old: 7..8,
|
||||
new: 7..11,
|
||||
},
|
||||
]);
|
||||
assert_eq!(patch.old_to_new(0), 0);
|
||||
assert_eq!(patch.old_to_new(1), 1);
|
||||
assert_eq!(patch.old_to_new(2), 2);
|
||||
assert_eq!(patch.old_to_new(3), 2);
|
||||
assert_eq!(patch.old_to_new(4), 4);
|
||||
assert_eq!(patch.old_to_new(5), 5);
|
||||
assert_eq!(patch.old_to_new(6), 6);
|
||||
assert_eq!(patch.old_to_new(7), 7);
|
||||
assert_eq!(patch.old_to_new(8), 11);
|
||||
assert_eq!(patch.old_to_new(9), 12);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_patch_compositions(mut rng: StdRng) {
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(20);
|
||||
|
||||
let initial_chars = (0..rng.random_range(0..=100))
|
||||
.map(|_| rng.random_range(b'a'..=b'z') as char)
|
||||
.collect::<Vec<_>>();
|
||||
log::info!("initial chars: {:?}", initial_chars);
|
||||
|
||||
// Generate two sequential patches
|
||||
let mut patches = Vec::new();
|
||||
let mut expected_chars = initial_chars.clone();
|
||||
for i in 0..2 {
|
||||
log::info!("patch {}:", i);
|
||||
|
||||
let mut delta = 0i32;
|
||||
let mut last_edit_end = 0;
|
||||
let mut edits = Vec::new();
|
||||
|
||||
for _ in 0..operations {
|
||||
if last_edit_end >= expected_chars.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let end = rng.random_range(last_edit_end..=expected_chars.len());
|
||||
let start = rng.random_range(last_edit_end..=end);
|
||||
let old_len = end - start;
|
||||
|
||||
let mut new_len = rng.random_range(0..=3);
|
||||
if start == end && new_len == 0 {
|
||||
new_len += 1;
|
||||
}
|
||||
|
||||
last_edit_end = start + new_len + 1;
|
||||
|
||||
let new_chars = (0..new_len)
|
||||
.map(|_| rng.random_range(b'A'..=b'Z') as char)
|
||||
.collect::<Vec<_>>();
|
||||
log::info!(
|
||||
" editing {:?}: {:?}",
|
||||
start..end,
|
||||
new_chars.iter().collect::<String>()
|
||||
);
|
||||
edits.push(Edit {
|
||||
old: (start as i32 - delta) as u32..(end as i32 - delta) as u32,
|
||||
new: start as u32..(start + new_len) as u32,
|
||||
});
|
||||
expected_chars.splice(start..end, new_chars);
|
||||
|
||||
delta += new_len as i32 - old_len as i32;
|
||||
}
|
||||
|
||||
patches.push(Patch(edits));
|
||||
}
|
||||
|
||||
log::info!("old patch: {:?}", &patches[0]);
|
||||
log::info!("new patch: {:?}", &patches[1]);
|
||||
log::info!("initial chars: {:?}", initial_chars);
|
||||
log::info!("final chars: {:?}", expected_chars);
|
||||
|
||||
// Compose the patches, and verify that it has the same effect as applying the
|
||||
// two patches separately.
|
||||
let composed = patches[0].compose(&patches[1]);
|
||||
log::info!("composed patch: {:?}", &composed);
|
||||
|
||||
let mut actual_chars = initial_chars;
|
||||
for edit in composed.0 {
|
||||
actual_chars.splice(
|
||||
edit.new.start as usize..edit.new.start as usize + edit.old.len(),
|
||||
expected_chars[edit.new.start as usize..edit.new.end as usize]
|
||||
.iter()
|
||||
.copied(),
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(actual_chars, expected_chars);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[allow(clippy::almost_complete_range)]
|
||||
fn assert_patch_composition(old: Patch<u32>, new: Patch<u32>, composed: Patch<u32>) {
|
||||
let original = ('a'..'z').collect::<Vec<_>>();
|
||||
let inserted = ('A'..'Z').collect::<Vec<_>>();
|
||||
|
||||
let mut expected = original.clone();
|
||||
apply_patch(&mut expected, &old, &inserted);
|
||||
apply_patch(&mut expected, &new, &inserted);
|
||||
|
||||
let mut actual = original;
|
||||
apply_patch(&mut actual, &composed, &expected);
|
||||
assert_eq!(
|
||||
actual.into_iter().collect::<String>(),
|
||||
expected.into_iter().collect::<String>(),
|
||||
"expected patch is incorrect"
|
||||
);
|
||||
|
||||
assert_eq!(old.compose(&new), composed);
|
||||
}
|
||||
|
||||
fn apply_patch(text: &mut Vec<char>, patch: &Patch<u32>, new_text: &[char]) {
|
||||
for edit in patch.0.iter().rev() {
|
||||
text.splice(
|
||||
edit.old.start as usize..edit.old.end as usize,
|
||||
new_text[edit.new.start as usize..edit.new.end as usize]
|
||||
.iter()
|
||||
.copied(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
169
crates/text/src/selection.rs
Normal file
169
crates/text/src/selection.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use crate::{Anchor, BufferSnapshot, TextDimension};
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Range;
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, PartialEq)]
|
||||
pub enum SelectionGoal {
|
||||
#[default]
|
||||
None,
|
||||
HorizontalPosition(f64),
|
||||
HorizontalRange {
|
||||
start: f64,
|
||||
end: f64,
|
||||
},
|
||||
WrappedHorizontalPosition((u32, f32)),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Selection<T> {
|
||||
pub id: usize,
|
||||
pub start: T,
|
||||
pub end: T,
|
||||
pub reversed: bool,
|
||||
pub goal: SelectionGoal,
|
||||
}
|
||||
|
||||
impl<T: Clone> Selection<T> {
|
||||
/// A place where the selection had stopped at.
|
||||
pub fn head(&self) -> T {
|
||||
if self.reversed {
|
||||
self.start.clone()
|
||||
} else {
|
||||
self.end.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A place where selection was initiated from.
|
||||
pub fn tail(&self) -> T {
|
||||
if self.reversed {
|
||||
self.end.clone()
|
||||
} else {
|
||||
self.start.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map<F, S>(&self, f: F) -> Selection<S>
|
||||
where
|
||||
F: Fn(T) -> S,
|
||||
{
|
||||
Selection::<S> {
|
||||
id: self.id,
|
||||
start: f(self.start.clone()),
|
||||
end: f(self.end.clone()),
|
||||
reversed: self.reversed,
|
||||
goal: self.goal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) {
|
||||
self.start = point.clone();
|
||||
self.end = point;
|
||||
self.goal = new_goal;
|
||||
self.reversed = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy + Ord> Selection<T> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.start == self.end
|
||||
}
|
||||
|
||||
pub fn set_head(&mut self, head: T, new_goal: SelectionGoal) {
|
||||
if head.cmp(&self.tail()) < Ordering::Equal {
|
||||
if !self.reversed {
|
||||
self.end = self.start;
|
||||
self.reversed = true;
|
||||
}
|
||||
self.start = head;
|
||||
} else {
|
||||
if self.reversed {
|
||||
self.start = self.end;
|
||||
self.reversed = false;
|
||||
}
|
||||
self.end = head;
|
||||
}
|
||||
self.goal = new_goal;
|
||||
}
|
||||
|
||||
pub fn set_tail(&mut self, tail: T, new_goal: SelectionGoal) {
|
||||
if tail.cmp(&self.head()) <= Ordering::Equal {
|
||||
if self.reversed {
|
||||
self.end = self.start;
|
||||
self.reversed = false;
|
||||
}
|
||||
self.start = tail;
|
||||
} else {
|
||||
if !self.reversed {
|
||||
self.start = self.end;
|
||||
self.reversed = true;
|
||||
}
|
||||
self.end = tail;
|
||||
}
|
||||
self.goal = new_goal;
|
||||
}
|
||||
|
||||
pub fn set_head_tail(&mut self, head: T, tail: T, new_goal: SelectionGoal) {
|
||||
if head < tail {
|
||||
self.reversed = true;
|
||||
self.start = head;
|
||||
self.end = tail;
|
||||
} else {
|
||||
self.reversed = false;
|
||||
self.start = tail;
|
||||
self.end = head;
|
||||
}
|
||||
self.goal = new_goal;
|
||||
}
|
||||
|
||||
pub fn swap_head_tail(&mut self) {
|
||||
if self.reversed {
|
||||
self.reversed = false;
|
||||
} else {
|
||||
std::mem::swap(&mut self.start, &mut self.end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> Selection<T> {
|
||||
pub fn range(&self) -> Range<T> {
|
||||
self.start..self.end
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::ops::Sub + Copy> Selection<T> {
|
||||
pub fn len(&self) -> <T as std::ops::Sub>::Output {
|
||||
self.end - self.start
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy + Eq> Selection<T> {
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn from_offset(offset: T) -> Self {
|
||||
Selection {
|
||||
id: 0,
|
||||
start: offset,
|
||||
end: offset,
|
||||
goal: SelectionGoal::None,
|
||||
reversed: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn equals(&self, offset_range: &Range<T>) -> bool {
|
||||
self.start == offset_range.start && self.end == offset_range.end
|
||||
}
|
||||
}
|
||||
|
||||
impl Selection<Anchor> {
|
||||
pub fn resolve<'a, D: 'a + TextDimension>(
|
||||
&'a self,
|
||||
snapshot: &'a BufferSnapshot,
|
||||
) -> Selection<D> {
|
||||
Selection {
|
||||
id: self.id,
|
||||
start: snapshot.summary_for_anchor(&self.start),
|
||||
end: snapshot.summary_for_anchor(&self.end),
|
||||
reversed: self.reversed,
|
||||
goal: self.goal,
|
||||
}
|
||||
}
|
||||
}
|
||||
67
crates/text/src/subscription.rs
Normal file
67
crates/text/src/subscription.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use crate::{Edit, Patch};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
mem,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Topic<T>(Mutex<Vec<Weak<Mutex<Patch<T>>>>>);
|
||||
|
||||
pub struct Subscription<T>(Arc<Mutex<Patch<T>>>);
|
||||
|
||||
impl<T: Default, TDelta> Topic<T>
|
||||
where
|
||||
T: 'static
|
||||
+ Copy
|
||||
+ Ord
|
||||
+ std::ops::Sub<T, Output = TDelta>
|
||||
+ std::ops::Add<TDelta, Output = T>
|
||||
+ std::ops::AddAssign<TDelta>
|
||||
+ Default,
|
||||
TDelta: Ord + Copy,
|
||||
{
|
||||
pub fn subscribe(&mut self) -> Subscription<T> {
|
||||
let subscription = Subscription(Default::default());
|
||||
self.0.get_mut().push(Arc::downgrade(&subscription.0));
|
||||
subscription
|
||||
}
|
||||
|
||||
pub fn publish(&self, edits: impl Clone + IntoIterator<Item = Edit<T>>) {
|
||||
publish(&mut self.0.lock(), edits);
|
||||
}
|
||||
|
||||
pub fn publish_mut(&mut self, edits: impl Clone + IntoIterator<Item = Edit<T>>) {
|
||||
publish(self.0.get_mut(), edits);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> Subscription<T> {
|
||||
pub fn consume(&self) -> Patch<T> {
|
||||
mem::take(&mut *self.0.lock())
|
||||
}
|
||||
}
|
||||
|
||||
fn publish<T, TDelta>(
|
||||
subscriptions: &mut Vec<Weak<Mutex<Patch<T>>>>,
|
||||
edits: impl Clone + IntoIterator<Item = Edit<T>>,
|
||||
) where
|
||||
T: 'static
|
||||
+ Copy
|
||||
+ Ord
|
||||
+ std::ops::Sub<T, Output = TDelta>
|
||||
+ std::ops::Add<TDelta, Output = T>
|
||||
+ std::ops::AddAssign<TDelta>
|
||||
+ Default,
|
||||
TDelta: Ord + Copy,
|
||||
{
|
||||
subscriptions.retain(|subscription| {
|
||||
if let Some(subscription) = subscription.upgrade() {
|
||||
let mut patch = subscription.lock();
|
||||
*patch = patch.compose(edits.clone());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
1057
crates/text/src/tests.rs
Normal file
1057
crates/text/src/tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
3744
crates/text/src/text.rs
Normal file
3744
crates/text/src/text.rs
Normal file
File diff suppressed because it is too large
Load Diff
115
crates/text/src/undo_map.rs
Normal file
115
crates/text/src/undo_map.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use crate::UndoOperation;
|
||||
use clock::Lamport;
|
||||
use std::cmp;
|
||||
use sum_tree::{Bias, SumTree};
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct UndoMapEntry {
|
||||
key: UndoMapKey,
|
||||
undo_count: u32,
|
||||
}
|
||||
|
||||
impl sum_tree::Item for UndoMapEntry {
|
||||
type Summary = UndoMapKey;
|
||||
|
||||
fn summary(&self, _cx: ()) -> Self::Summary {
|
||||
self.key
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::KeyedItem for UndoMapEntry {
|
||||
type Key = UndoMapKey;
|
||||
|
||||
fn key(&self) -> Self::Key {
|
||||
self.key
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct UndoMapKey {
|
||||
edit_id: clock::Lamport,
|
||||
undo_id: clock::Lamport,
|
||||
}
|
||||
|
||||
impl sum_tree::ContextLessSummary for UndoMapKey {
|
||||
fn zero() -> Self {
|
||||
UndoMapKey {
|
||||
edit_id: Lamport::MIN,
|
||||
undo_id: Lamport::MIN,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &Self) {
|
||||
*self = cmp::max(*self, *summary);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UndoMap(SumTree<UndoMapEntry>);
|
||||
|
||||
impl UndoMap {
|
||||
pub fn insert(&mut self, undo: &UndoOperation) {
|
||||
let edits = undo
|
||||
.counts
|
||||
.iter()
|
||||
.map(|(edit_id, count)| {
|
||||
sum_tree::Edit::Insert(UndoMapEntry {
|
||||
key: UndoMapKey {
|
||||
edit_id: *edit_id,
|
||||
undo_id: undo.timestamp,
|
||||
},
|
||||
undo_count: *count,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.0.edit(edits, ());
|
||||
}
|
||||
|
||||
pub fn is_undone(&self, edit_id: clock::Lamport) -> bool {
|
||||
self.undo_count(edit_id) % 2 == 1
|
||||
}
|
||||
pub fn was_undone(&self, edit_id: clock::Lamport, version: &clock::Global) -> bool {
|
||||
let mut cursor = self.0.cursor::<UndoMapKey>(());
|
||||
cursor.seek(
|
||||
&UndoMapKey {
|
||||
edit_id,
|
||||
undo_id: Lamport::MIN,
|
||||
},
|
||||
Bias::Left,
|
||||
);
|
||||
|
||||
let mut undo_count = 0;
|
||||
for entry in cursor {
|
||||
if entry.key.edit_id != edit_id {
|
||||
break;
|
||||
}
|
||||
|
||||
if version.observed(entry.key.undo_id) {
|
||||
undo_count = cmp::max(undo_count, entry.undo_count);
|
||||
}
|
||||
}
|
||||
|
||||
undo_count % 2 == 1
|
||||
}
|
||||
|
||||
pub fn undo_count(&self, edit_id: clock::Lamport) -> u32 {
|
||||
let mut cursor = self.0.cursor::<UndoMapKey>(());
|
||||
cursor.seek(
|
||||
&UndoMapKey {
|
||||
edit_id,
|
||||
undo_id: Lamport::MIN,
|
||||
},
|
||||
Bias::Left,
|
||||
);
|
||||
|
||||
let mut undo_count = 0;
|
||||
for entry in cursor {
|
||||
if entry.key.edit_id != edit_id {
|
||||
break;
|
||||
}
|
||||
|
||||
undo_count = cmp::max(undo_count, entry.undo_count);
|
||||
}
|
||||
undo_count
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user