diff --git a/Cargo.lock b/Cargo.lock index 06fc89c..09e7616 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -533,7 +533,7 @@ dependencies = [ [[package]] name = "yrb" -version = "0.5.5" +version = "0.5.6" dependencies = [ "magnus", "rb-sys", diff --git a/ext/yrb/Cargo.toml b/ext/yrb/Cargo.toml index 7b655db..a4552e7 100644 --- a/ext/yrb/Cargo.toml +++ b/ext/yrb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yrb" -version = "0.5.5" +version = "0.5.6" authors = ["Hannes Moser "] edition = "2021" homepage = "https://github.com/y-crdt/yrb" diff --git a/ext/yrb/src/lib.rs b/ext/yrb/src/lib.rs index 29417da..e7d59d4 100644 --- a/ext/yrb/src/lib.rs +++ b/ext/yrb/src/lib.rs @@ -2,6 +2,7 @@ extern crate core; use crate::yarray::YArray; use crate::yawareness::{YAwareness, YAwarenessEvent}; +use crate::ydiff::YDiff; use crate::ydoc::YDoc; use crate::ymap::YMap; use crate::ytext::YText; @@ -9,6 +10,7 @@ use crate::ytransaction::YTransaction; use crate::yxml_element::YXmlElement; use crate::yxml_fragment::YXmlFragment; use crate::yxml_text::YXmlText; + use magnus::{class, define_module, function, method, Error, Module, Object}; mod utils; @@ -16,6 +18,7 @@ mod yany; mod yarray; mod yattrs; mod yawareness; +mod ydiff; mod ydoc; mod ymap; mod ytext; @@ -224,6 +227,9 @@ fn init() -> Result<(), Error> { .define_class("Text", class::object()) .expect("cannot define class Y::Text"); + ytext + .define_private_method("ytext_diff", method!(YText::ytext_diff, 1)) + .expect("cannot define private method: ytext_diff"); ytext .define_private_method("ytext_format", method!(YText::ytext_format, 4)) .expect("cannot define private method: ytext_format"); @@ -618,7 +624,7 @@ fn init() -> Result<(), Error> { let yawareness_event = module .define_class("AwarenessEvent", class::object()) - .expect("cannot define class Y:AwarenessEvent"); + .expect("cannot define class Y::AwarenessEvent"); yawareness_event .define_method("added", method!(YAwarenessEvent::added, 0)) .expect("cannot define private method: added"); @@ -629,5 +635,15 @@ fn init() -> Result<(), Error> { .define_method("removed", method!(YAwarenessEvent::removed, 0)) .expect("cannot define private method: removed"); + let ydiff = module + .define_class("Diff", class::object()) + .expect("cannot define class Y::Diff"); + ydiff + .define_private_method("ydiff_insert", method!(YDiff::ydiff_insert, 0)) + .expect("cannot define private method: insert"); + ydiff + .define_private_method("ydiff_attrs", method!(YDiff::ydiff_attrs, 0)) + .expect("cannot define private method: attrs"); + Ok(()) } diff --git a/ext/yrb/src/yattrs.rs b/ext/yrb/src/yattrs.rs index 6cb346f..d60d920 100644 --- a/ext/yrb/src/yattrs.rs +++ b/ext/yrb/src/yattrs.rs @@ -1,16 +1,21 @@ use crate::yvalue::YValue; use magnus::r_hash::ForEach::Continue; use magnus::{RHash, Value}; -use std::ops::{Deref, DerefMut}; +use std::cell::RefCell; use std::sync::Arc; use yrs::types::Attrs; use yrs::Any; -pub(crate) struct YAttrs(pub(crate) Attrs); +#[magnus::wrap(class = "Y::Attrs")] +#[derive(Clone)] +pub(crate) struct YAttrs(pub(crate) RefCell); + +/// SAFETY: This is safe because we only access this data when the GVL is held. +unsafe impl Send for YAttrs {} impl From for YAttrs { fn from(value: Attrs) -> Self { - YAttrs(value) + YAttrs(RefCell::from(value)) } } @@ -29,20 +34,6 @@ impl From for YAttrs { }) .expect("cannot iterate attributes hash"); - YAttrs(attrs) - } -} - -impl Deref for YAttrs { - type Target = Attrs; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for YAttrs { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + YAttrs(RefCell::from(attrs)) } } diff --git a/ext/yrb/src/ydiff.rs b/ext/yrb/src/ydiff.rs new file mode 100644 index 0000000..b97413a --- /dev/null +++ b/ext/yrb/src/ydiff.rs @@ -0,0 +1,19 @@ +use magnus::{IntoValue, RHash, Value}; + +unsafe impl Send for YDiff {} + +#[magnus::wrap(class = "Y::Diff")] +pub(crate) struct YDiff { + pub(crate) ydiff_insert: Value, + pub(crate) ydiff_attrs: Option, +} + +impl YDiff { + pub(crate) fn ydiff_insert(&self) -> Value { + self.ydiff_insert + } + + pub(crate) fn ydiff_attrs(&self) -> Option { + self.ydiff_attrs.as_ref().map(|value| value.into_value()) + } +} diff --git a/ext/yrb/src/ytext.rs b/ext/yrb/src/ytext.rs index 9d50a52..181d6e3 100644 --- a/ext/yrb/src/ytext.rs +++ b/ext/yrb/src/ytext.rs @@ -1,10 +1,13 @@ use crate::yattrs::YAttrs; +use crate::ydiff::YDiff; use crate::yvalue::YValue; use crate::YTransaction; use magnus::block::Proc; use magnus::value::Qnil; -use magnus::{Error, RHash, Symbol, Value}; +use magnus::RArray; +pub(crate) use magnus::{Error, IntoValue, RHash, Symbol, Value}; use std::cell::RefCell; +use yrs::types::text::YChange; use yrs::types::Delta; use yrs::{Any, GetString, Observable, Text, TextRef}; @@ -15,6 +18,38 @@ pub(crate) struct YText(pub(crate) RefCell); unsafe impl Send for YText {} impl YText { + pub(crate) fn ytext_diff(&self, transaction: &YTransaction) -> RArray { + let tx = transaction.transaction(); + let tx = tx.as_ref().unwrap(); + + RArray::from_iter( + self.0 + .borrow() + .diff(tx, YChange::identity) + .iter() + .map(move |diff| { + let yvalue = YValue::from(diff.insert.clone()); + let insert = yvalue.0.into_inner(); + let attributes = diff.attributes.as_ref().map_or_else( + || None, + |boxed_attrs| { + let attributes = RHash::new(); + for (key, value) in boxed_attrs.iter() { + let key = key.to_string(); + let value = YValue::from(value.clone()).0.into_inner(); + attributes.aset(key, value).expect("cannot add value"); + } + Some(attributes) + }, + ); + YDiff { + ydiff_insert: insert, + ydiff_attrs: attributes, + } + .into_value() + }), + ) + } pub(crate) fn ytext_format( &self, transaction: &YTransaction, @@ -27,7 +62,9 @@ impl YText { let a = YAttrs::from(attrs); - self.0.borrow_mut().format(tx, index, length, a.0) + self.0 + .borrow_mut() + .format(tx, index, length, a.0.into_inner()) } pub(crate) fn ytext_insert(&self, transaction: &YTransaction, index: u32, chunk: String) { let mut tx = transaction.transaction(); @@ -66,7 +103,7 @@ impl YText { self.0 .borrow_mut() - .insert_embed_with_attributes(tx, index, avalue, a.0); + .insert_embed_with_attributes(tx, index, avalue, a.0.into_inner()); } pub(crate) fn ytext_insert_with_attributes( &self, @@ -82,7 +119,7 @@ impl YText { self.0 .borrow_mut() - .insert_with_attributes(tx, index, chunk.as_str(), a.0) + .insert_with_attributes(tx, index, chunk.as_str(), a.0.into_inner()) } pub(crate) fn ytext_length(&self, transaction: &YTransaction) -> u32 { let tx = transaction.transaction(); diff --git a/lib/y-rb.rb b/lib/y-rb.rb index e86b0b5..cd6c59b 100644 --- a/lib/y-rb.rb +++ b/lib/y-rb.rb @@ -11,6 +11,7 @@ require_relative "y/array" require_relative "y/awareness" +require_relative "y/diff" require_relative "y/doc" require_relative "y/map" require_relative "y/text" diff --git a/lib/y/array.rb b/lib/y/array.rb index 47b8e83..22d8fe0 100644 --- a/lib/y/array.rb +++ b/lib/y/array.rb @@ -115,8 +115,8 @@ def detach(subscription_id) end # @return [void] - def each(&block) - document.current_transaction { |tx| yarray_each(tx, &block) } + def each(...) + document.current_transaction { |tx| yarray_each(tx, ...) } end # Check if the array is empty diff --git a/lib/y/diff.rb b/lib/y/diff.rb new file mode 100644 index 0000000..01c4499 --- /dev/null +++ b/lib/y/diff.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Y + # A representation of an uniformly-formatted chunk of rich context stored by + # Text or XmlText. It contains a value (which could be a string, embedded + # object or another shared type) with optional formatting attributes wrapping + # around this chunk. + class Diff + # @return [Object] + def insert + ydiff_insert + end + + # @return [Hash] + def attrs + ydiff_attrs + end + + # Convert the diff to a Hash representation + # + # @return [Hash] + def to_h + { + insert: ydiff_insert, + attrs: ydiff_attrs + } + end + + # @!method ydiff_insert() + # Returns string representation of text + # + # @return [Object] + + # @!method ydiff_attrs() + # + # @return [Hash] + end +end diff --git a/lib/y/text.rb b/lib/y/text.rb index 15b92f8..6289ff0 100644 --- a/lib/y/text.rb +++ b/lib/y/text.rb @@ -80,6 +80,15 @@ def detach(subscription_id) ytext_unobserve(subscription_id) end + # Diff + # + # @return[Array] + def diff + document.current_transaction do |tx| + ytext_diff(tx) + end + end + # Checks if text is empty # # @example Check if text is empty @@ -284,6 +293,12 @@ def can_insert?(value) value.is_a?(Hash) end + # @!method ytext_diff(tx) + # Returns text changes as list of diffs + # + # @param transaction [Y::Transaction] + # @return [Array] + # @!method ytext_insert(tx, index, chunk) # Insert into text at position # diff --git a/lib/y/version.rb b/lib/y/version.rb index 21b855f..c31a70f 100644 --- a/lib/y/version.rb +++ b/lib/y/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Y - VERSION = "0.5.5" + VERSION = "0.5.6" end diff --git a/spec/y/text_spec.rb b/spec/y/text_spec.rb index 4b78789..452b712 100644 --- a/spec/y/text_spec.rb +++ b/spec/y/text_spec.rb @@ -20,6 +20,21 @@ expect(text.to_s).to eq("Hello, World!") end + it "get a list of changes" do + doc = Y::Doc.new + text = doc.get_text("my text") + + text.insert(0, "Hello, World!", { format: "bold" }) + text.insert(13, " From Hannes.", { format: "italic" }) + + expect(text.diff.map(&:to_h)).to eq([ + { insert: "Hello, World!", + attrs: { "format" => "bold" } }, + { insert: " From Hannes.", + attrs: { "format" => "italic" } } + ]) + end + it "inserts string at position" do doc = Y::Doc.new text = doc.get_text("my text")