Skip to content

Commit

Permalink
exploration to abstract structure needed for span
Browse files Browse the repository at this point in the history
  • Loading branch information
heckj committed Jun 17, 2024
1 parent 52414f7 commit e4e0f8a
Show file tree
Hide file tree
Showing 8 changed files with 708 additions and 27 deletions.
464 changes: 443 additions & 21 deletions AutomergeUniffi/automerge.swift

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions Sources/Automerge/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,25 @@ public final class Document: @unchecked Sendable {
try marksAt(obj: obj, position: position, heads: heads())
}

public func splitBlock(obj: ObjId, index: UInt64) throws -> ObjId {
try sync {
try self.doc.wrapErrors { doc in
sendObjectWillChange()
let objIdBytes = try doc.splitBlock(obj: obj.bytes, index: index)
return ObjId(bytes: objIdBytes)
}
}
}

public func joinBlock(obj: ObjId, index: UInt64) throws {
try sync {
try self.doc.wrapErrors { doc in
sendObjectWillChange()
try doc.joinBlock(obj: obj.bytes, index: index)
}
}
}

/// Commit the auto-generated transaction with options.
///
/// - Parameters:
Expand Down
36 changes: 34 additions & 2 deletions Sources/_CAutomergeUniffi/include/automergeFFI.h
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,26 @@ typedef struct UniffiForeignFutureStructVoid {
typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureStructVoid
);

#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_CLONE_AMVALUE
#define UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_CLONE_AMVALUE
void*_Nonnull uniffi_uniffi_automerge_fn_clone_amvalue(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_FREE_AMVALUE
#define UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_FREE_AMVALUE
void uniffi_uniffi_automerge_fn_free_amvalue(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_CONSTRUCTOR_AMVALUE_NEW
#define UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_CONSTRUCTOR_AMVALUE_NEW
void*_Nonnull uniffi_uniffi_automerge_fn_constructor_amvalue_new(RustBuffer input, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_CONSTRUCTOR_AMVALUE_NEW_FROM_MAP
#define UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_CONSTRUCTOR_AMVALUE_NEW_FROM_MAP
void*_Nonnull uniffi_uniffi_automerge_fn_constructor_amvalue_new_from_map(RustBuffer input, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_CLONE_DOC
#define UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_CLONE_DOC
Expand Down Expand Up @@ -434,7 +454,7 @@ RustBuffer uniffi_uniffi_automerge_fn_method_doc_insert_object_in_list(void*_Non
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_METHOD_DOC_JOIN_BLOCK
#define UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_METHOD_DOC_JOIN_BLOCK
void uniffi_uniffi_automerge_fn_method_doc_join_block(void*_Nonnull ptr, RustBuffer obj, uint32_t index, RustCallStatus *_Nonnull out_status
void uniffi_uniffi_automerge_fn_method_doc_join_block(void*_Nonnull ptr, RustBuffer obj, uint64_t index, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_METHOD_DOC_LENGTH
Expand Down Expand Up @@ -559,7 +579,7 @@ void uniffi_uniffi_automerge_fn_method_doc_splice_text(void*_Nonnull ptr, RustBu
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_METHOD_DOC_SPLIT_BLOCK
#define UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_METHOD_DOC_SPLIT_BLOCK
RustBuffer uniffi_uniffi_automerge_fn_method_doc_split_block(void*_Nonnull ptr, RustBuffer obj, uint32_t index, RustCallStatus *_Nonnull out_status
RustBuffer uniffi_uniffi_automerge_fn_method_doc_split_block(void*_Nonnull ptr, RustBuffer obj, uint64_t index, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_FN_METHOD_DOC_TEXT
Expand Down Expand Up @@ -1303,6 +1323,18 @@ uint16_t uniffi_uniffi_automerge_checksum_method_syncstate_reset(void
#define UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_CHECKSUM_METHOD_SYNCSTATE_THEIR_HEADS
uint16_t uniffi_uniffi_automerge_checksum_method_syncstate_their_heads(void

);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_CHECKSUM_CONSTRUCTOR_AMVALUE_NEW
#define UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_CHECKSUM_CONSTRUCTOR_AMVALUE_NEW
uint16_t uniffi_uniffi_automerge_checksum_constructor_amvalue_new(void

);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_CHECKSUM_CONSTRUCTOR_AMVALUE_NEW_FROM_MAP
#define UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_CHECKSUM_CONSTRUCTOR_AMVALUE_NEW_FROM_MAP
uint16_t uniffi_uniffi_automerge_checksum_constructor_amvalue_new_from_map(void

);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_UNIFFI_AUTOMERGE_CHECKSUM_CONSTRUCTOR_DOC_LOAD
Expand Down
46 changes: 46 additions & 0 deletions Tests/AutomergeTests/TestBlocks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@testable import Automerge
import XCTest

class BlocksTestCase: XCTestCase {
func testSplitBlock() async throws {
// replicating test from https://github.com/automerge/automerge/blob/main/rust/automerge-wasm/test/blocks.mts#L8
// to verify interactions

// although looking through it, the test at
// https://github.com/automerge/automerge/blob/main/rust/automerge/tests/block_tests.rs#L11
// would make a lot more sense...
let doc = Document()
let text = try! doc.putObject(obj: ObjId.ROOT, key: "example", ty: ObjType.Text)
try doc.updateText(obj: text, value: "🐻🐻🐻bbbccc")
let result = try doc.splitBlock(obj: text, index: 6)
// try doc.walk()
}

/*
it("can split a block", () => {
const doc = create({ actor: "aabbcc" })
const text = doc.putObject("_root", "list", "🐻🐻🐻bbbccc")
doc.splitBlock(text, 6, { type: "li", parents: ["ul"], attrs: {kind: "todo" }});

NOTE(heckj):
^^ JS API wraps two calls in Rust api- first splitting the block, second updating the block that was just split

const spans = doc.spans("/list");
console.log(JSON.stringify(spans))
assert.deepStrictEqual(spans, [
{ type: "text", value: "🐻🐻🐻" },
{ type: 'block', value: { type: 'li', parents: ['ul'], attrs: {kind: "todo"} } },
{ type: 'text', value: 'bbbccc' }
])
})

*/

func testJoinBlock() async throws {
let doc = Document()
let text = try! doc.putObject(obj: ObjId.ROOT, key: "example", ty: ObjType.Text)
try doc.updateText(obj: text, value: "🐻🐻🐻bbbccc")
let result = try doc.splitBlock(obj: text, index: 6)
try doc.joinBlock(obj: text, index: 6)
}
}
42 changes: 40 additions & 2 deletions rust/src/automerge.udl
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,44 @@ dictionary Mark {
ScalarValue value;
};

dictionary MarkSet {
record<string,ScalarValue> marks;
};

enum AMValueType {
"Scalar",
"List",
"Map",
"Text",
};

interface AMValue {
constructor(ScalarValue input);
[Name=new_from_map]
constructor(MapValue input);
};

dictionary MapValue {
record<string,ScalarValue> value;
};

dictionary TextValue {
string value;
record<string,ScalarValue> marks;
};

dictionary ListValue {
AMValue value;
record<string,ScalarValue> marks;
boolean conflict;
};

[Enum]
interface Span {
Text ( string text, MarkSet? marks );
Block ( MapValue value );
};

dictionary PathElement {
Prop prop;
ObjId obj;
Expand Down Expand Up @@ -180,9 +218,9 @@ interface Doc {
sequence<Mark> marks_at_position(ObjId obj, Position position, sequence<ChangeHash> heads);

[Throws=DocError]
ObjId split_block(ObjId obj, u32 index);
ObjId split_block(ObjId obj, u64 index);
[Throws=DocError]
void join_block(ObjId obj, u32 index);
void join_block(ObjId obj, u64 index);

[Throws=DocError]
void delete_in_map(ObjId obj, string key);
Expand Down
4 changes: 2 additions & 2 deletions rust/src/doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,14 +500,14 @@ impl Doc {
Ok(Mark::from_markset(markset, index as u64))
}

pub fn split_block(&self, obj: ObjId, index: u32) -> Result<ObjId, DocError> {
pub fn split_block(&self, obj: ObjId, index: u64) -> Result<ObjId, DocError> {
let mut doc = self.0.write().unwrap();
let obj = am::ObjId::from(obj);
let id = doc.split_block(obj, index.try_into().unwrap())?;
Ok(id.into())
}

pub fn join_block(&self, obj: ObjId, index: u32) -> Result<(), DocError> {
pub fn join_block(&self, obj: ObjId, index: u64) -> Result<(), DocError> {
let mut doc = self.0.write().unwrap();
let obj = am::ObjId::from(obj);
doc.join_block(obj, index.try_into().unwrap())?;
Expand Down
2 changes: 2 additions & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ mod sync_state;
use sync_state::{DecodeSyncStateError, SyncState};
mod value;
use value::Value;
mod span;
use span::{AMValue, AMValueType, ListValue, MapValue, MarkSet, Span, TextValue};
122 changes: 122 additions & 0 deletions rust/src/span.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use crate::ScalarValue;
use std::collections::HashMap;
use std::sync::Arc;
// use automerge as am;

// ?? not entirely clear on how to expose MarkSet - nothing in the existing API exposes
// a map-like type through uniFFI - it's all been enums, lists, and structs so far.
// in am, MarkSet uses BTreeMap, which is a standard collection type, indexed with smolStr
// pub struct BlockLikeThing {
// pub parents: Vec<String>, // as Vec<AMValue> to convert through hydrate?
// pub r#type: String,
// pub attr: HashMap<String, AMValue>, // as MapValue
// }

// maps to am::iter::Span
// need to create From<> in to convert over
pub enum Span {
/// A span of text and the marks that were active for that span
Text {
text: String,
marks: Option<MarkSet>,
},
/// A block marker
Block { value: MapValue },
}

// loosely maps to am::marks:MarkSet
// need to create From<> in to convert over
pub struct MarkSet {
pub marks: HashMap<String, ScalarValue>,
}

// loosely maps to am::hydrate::Value
// need to create From<> in to convert over
pub enum AMValueType {
Scalar,
Map,
List,
Text,
}

// Joe's hacky version of Swift's "Indirect enum" setup - where it's always
// a reference type. The UniFFI UDL doesn't appear to allow us to model that,
// so I've backed in to implementing each instance of this 'generic tree' enumeration
// setup as an Object through the UDL interface (this translates to a Class instance in
// Swift)
//
// https://mozilla.github.io/uniffi-rs/udl/interfaces.html
// The UniFFI FFI interface expects that to be in the form of Arc<Something> due to
// its use of proxy objects.
// Details on how UniFFI manages it's object references at
// https://mozilla.github.io/uniffi-rs/internals/object_references.html
//
// Enums in Rust are concrete, there doesn't appear to be a direct
// mapping to what is (in Swift) an indirect enum where the data associated with
// an enum case is a reference to some other type, dependent upon the case.
// I previously tried to set this general object structure up as a tree of enums, but I
// hit multiple limits within the UDL representation:
// - because enums are concrete, you can't have a self-referential enum (Rust compiler fails
// that as an "infinite enum").
// - I tried to break that infinite struct up by adding a reference type,
// but a reference counted instance isn't allowed inside the UDL enum structure (by Ref not supported).
// - I tried making List (to be an object holding a Vec<List>) that I could use by reference,
// but then learned that Object types also aren't supported in Enum.
//
// Based on that, I think AMValue may need to be represented as an object, and the manually handling
// the type of value by an Enum that _is_ concrete, but any assocaited data something external
// to that enum that the object manages - since we've got a set of 4 possible values here,
// maybe 4 optional types, pushing the work to know what's returned to the developer? There may be
// other idiomatic patterns that could be used, but I'm not spotting what else might be possible
// right now, at least through the lens of what's allowed by the UDL.

pub struct AMValue {
kind: AMValueType,
scalar_value: Option<ScalarValue>,
map_value: Option<MapValue>,
list_value: Option<Vec<ListValue>>,
text_value: Option<TextValue>,
}

impl AMValue {
pub fn new(input: ScalarValue) -> Self {
AMValue {
kind: AMValueType::Scalar,
scalar_value: Some(input),
map_value: None,
list_value: None,
text_value: None,
}
}

pub fn new_from_map(input: MapValue) -> Self {
AMValue {
kind: AMValueType::Map,
scalar_value: None,
map_value: Some(input),
list_value: None,
text_value: None,
}
}
}

// made an explicit type of this so that it could be referenced
// in Span as the internal data for a Block.
pub struct MapValue {
pub value: HashMap<String, ScalarValue>,
}

// loosely maps to am::hydrate::ListValue
// need to create From<> in to convert over
pub struct ListValue {
pub value: Arc<AMValue>,
pub marks: HashMap<String, ScalarValue>,
pub conflict: bool,
}

// loosely maps to am::hydrate::Text
// need to create From<> in to convert over
pub struct TextValue {
pub value: String,
pub marks: HashMap<String, ScalarValue>,
}

0 comments on commit e4e0f8a

Please sign in to comment.