Skip to content

Commit

Permalink
feat: add support for text diff
Browse files Browse the repository at this point in the history
  • Loading branch information
eliias committed Mar 10, 2024
1 parent 605bbaa commit e45e34b
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 28 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ext/yrb/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "yrb"
version = "0.5.5"
version = "0.5.6"
authors = ["Hannes Moser <[email protected]>"]
edition = "2021"
homepage = "https://github.com/y-crdt/yrb"
Expand Down
18 changes: 17 additions & 1 deletion ext/yrb/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@ 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;
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;
mod yany;
mod yarray;
mod yattrs;
mod yawareness;
mod ydiff;
mod ydoc;
mod ymap;
mod ytext;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand All @@ -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(())
}
27 changes: 9 additions & 18 deletions ext/yrb/src/yattrs.rs
Original file line number Diff line number Diff line change
@@ -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<Attrs>);

/// SAFETY: This is safe because we only access this data when the GVL is held.
unsafe impl Send for YAttrs {}

impl From<Attrs> for YAttrs {
fn from(value: Attrs) -> Self {
YAttrs(value)
YAttrs(RefCell::from(value))
}
}

Expand All @@ -29,20 +34,6 @@ impl From<RHash> 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))
}
}
19 changes: 19 additions & 0 deletions ext/yrb/src/ydiff.rs
Original file line number Diff line number Diff line change
@@ -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<RHash>,
}

impl YDiff {
pub(crate) fn ydiff_insert(&self) -> Value {
self.ydiff_insert
}

pub(crate) fn ydiff_attrs(&self) -> Option<Value> {
self.ydiff_attrs.as_ref().map(|value| value.into_value())
}
}
45 changes: 41 additions & 4 deletions ext/yrb/src/ytext.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -15,6 +18,38 @@ pub(crate) struct YText(pub(crate) RefCell<TextRef>);
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,
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand Down
1 change: 1 addition & 0 deletions lib/y-rb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions lib/y/array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions lib/y/diff.rb
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions lib/y/text.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ def detach(subscription_id)
ytext_unobserve(subscription_id)
end

# Diff
#
# @return[Array<YDiff>]
def diff
document.current_transaction do |tx|
ytext_diff(tx)
end
end

# Checks if text is empty
#
# @example Check if text is empty
Expand Down Expand Up @@ -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<YDiff>]

# @!method ytext_insert(tx, index, chunk)
# Insert into text at position
#
Expand Down
2 changes: 1 addition & 1 deletion lib/y/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Y
VERSION = "0.5.5"
VERSION = "0.5.6"
end
15 changes: 15 additions & 0 deletions spec/y/text_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit e45e34b

Please sign in to comment.