Ver código fonte

Implement proper transformation for cursors

Eric Zhang 4 anos atrás
pai
commit
64d0b632ac

+ 1 - 0
rustpad-server/src/lib.rs

@@ -11,6 +11,7 @@ use rustpad::Rustpad;
 use tokio::time::{self, Instant};
 use warp::{filters::BoxedFilter, ws::Ws, Filter, Reply};
 
+mod ot;
 mod rustpad;
 
 /// An entry stored in the global server map.

+ 23 - 0
rustpad-server/src/ot.rs

@@ -0,0 +1,23 @@
+//! Helper methods for working with operational transformation.
+
+use operational_transform::{Operation, OperationSeq};
+
+/// Return the new index of a position in the string.
+pub fn transform_index(operation: &OperationSeq, position: u32) -> u32 {
+    let mut index = position as i32;
+    let mut new_index = index;
+    for op in operation.ops() {
+        match op {
+            &Operation::Retain(n) => index -= n as i32,
+            Operation::Insert(s) => new_index += s.len() as i32,
+            &Operation::Delete(n) => {
+                new_index -= std::cmp::min(index, n as i32);
+                index -= n as i32;
+            }
+        }
+        if index < 0 {
+            break;
+        }
+    }
+    new_index as u32
+}

+ 22 - 5
rustpad-server/src/rustpad.rs

@@ -12,6 +12,8 @@ use serde::{Deserialize, Serialize};
 use tokio::sync::{broadcast, Notify};
 use warp::ws::{Message, WebSocket};
 
+use crate::ot::transform_index;
+
 /// The main object representing a collaborative session.
 pub struct Rustpad {
     /// State modified by critical sections of the code.
@@ -136,8 +138,7 @@ impl Rustpad {
     async fn handle_connection(&self, id: u64, mut socket: WebSocket) -> Result<()> {
         let mut update_rx = self.update.subscribe();
 
-        self.send_initial(id, &mut socket).await?;
-        let mut revision: usize = 0;
+        let mut revision: usize = self.send_initial(id, &mut socket).await?;
 
         loop {
             // In order to avoid the "lost wakeup" problem, we first request a
@@ -167,11 +168,17 @@ impl Rustpad {
         Ok(())
     }
 
-    async fn send_initial(&self, id: u64, socket: &mut WebSocket) -> Result<()> {
+    async fn send_initial(&self, id: u64, socket: &mut WebSocket) -> Result<usize> {
         socket.send(ServerMsg::Identity(id).into()).await?;
         let mut messages = Vec::new();
-        {
+        let revision = {
             let state = self.state.read();
+            if !state.operations.is_empty() {
+                messages.push(ServerMsg::History {
+                    start: 0,
+                    operations: state.operations.clone(),
+                });
+            }
             if let Some(language) = &state.language {
                 messages.push(ServerMsg::Language(language.clone()));
             }
@@ -187,11 +194,12 @@ impl Rustpad {
                     data: data.clone(),
                 });
             }
+            state.operations.len()
         };
         for msg in messages {
             socket.send(msg.into()).await?;
         }
-        Ok(())
+        Ok(revision)
     }
 
     async fn send_history(&self, start: usize, socket: &mut WebSocket) -> Result<usize> {
@@ -271,6 +279,15 @@ impl Rustpad {
         }
         let new_text = operation.apply(&state.text)?;
         let mut state = RwLockUpgradableReadGuard::upgrade(state);
+        for (_, data) in state.cursors.iter_mut() {
+            for cursor in data.cursors.iter_mut() {
+                *cursor = transform_index(&operation, *cursor);
+            }
+            for (start, end) in data.selections.iter_mut() {
+                *start = transform_index(&operation, *start);
+                *end = transform_index(&operation, *end);
+            }
+        }
         state.operations.push(UserOperation { id, operation });
         state.text = new_text;
         Ok(())

+ 20 - 1
rustpad-server/tests/users.rs

@@ -155,9 +155,28 @@ async fn test_cursors() -> Result<()> {
     client.send(&json!({ "Invalid": "please close" })).await;
     client.recv_closed().await?;
 
+    let msg = json!({
+        "Edit": {
+            "revision": 0,
+            "operation": ["a"]
+        }
+    });
+    client2.send(&msg).await;
+
     let mut client3 = connect(&filter, "foobar").await?;
     assert_eq!(client3.recv().await?, json!({ "Identity": 2 }));
-    assert_eq!(client3.recv().await?, cursors2_resp);
+    client3.recv().await?;
+
+    let transformed_cursors2_resp = json!({
+        "UserCursor": {
+            "id": 1,
+            "data": {
+                "cursors": [11],
+                "selections": []
+            }
+        }
+    });
+    assert_eq!(client3.recv().await?, transformed_cursors2_resp);
 
     Ok(())
 }

+ 21 - 0
rustpad-wasm/src/lib.rs

@@ -133,6 +133,27 @@ impl OpSeq {
         self.0.target_len()
     }
 
+    /// Return the new index of a position in the string.
+    pub fn transform_index(&self, position: u32) -> u32 {
+        let mut index = position as i32;
+        let mut new_index = index;
+        for op in self.0.ops() {
+            use operational_transform::Operation::*;
+            match op {
+                &Retain(n) => index -= n as i32,
+                Insert(s) => new_index += s.len() as i32,
+                &Delete(n) => {
+                    new_index -= std::cmp::min(index, n as i32);
+                    index -= n as i32;
+                }
+            }
+            if index < 0 {
+                break;
+            }
+        }
+        new_index as u32
+    }
+
     /// Attempts to deserialize an `OpSeq` from a JSON string.
     pub fn from_str(s: &str) -> Option<OpSeq> {
         serde_json::from_str(s).ok()

+ 13 - 0
rustpad-wasm/tests/web.rs

@@ -50,3 +50,16 @@ fn invert_operations() {
     let p = o.invert(s);
     assert_eq!(p.apply(&o.apply(s).unwrap()).unwrap(), s);
 }
+
+#[wasm_bindgen_test]
+fn transform_index() {
+    let mut o = OpSeq::default();
+    o.retain(3);
+    o.insert("def");
+    o.retain(3);
+    o.insert("abc");
+    assert_eq!(o.transform_index(2), 2);
+    assert_eq!(o.transform_index(3), 6);
+    assert_eq!(o.transform_index(5), 8);
+    assert_eq!(o.transform_index(7), 13);
+}

+ 14 - 0
src/rustpad.ts

@@ -230,6 +230,7 @@ class Rustpad {
     } else {
       this.buffer = this.buffer.compose(operation);
     }
+    this.transformCursors(operation);
   }
 
   private sendOperation(operation: OpSeq) {
@@ -306,6 +307,19 @@ class Rustpad {
 
     this.lastValue = this.model.getValue();
     this.ignoreChanges = false;
+
+    this.transformCursors(operation);
+  }
+
+  private transformCursors(operation: OpSeq) {
+    for (const data of Object.values(this.userCursors)) {
+      data.cursors = data.cursors.map((c) => operation.transform_index(c));
+      data.selections = data.selections.map(([s, e]) => [
+        operation.transform_index(s),
+        operation.transform_index(e),
+      ]);
+    }
+    this.updateCursors();
   }
 
   private updateCursors() {