From e4e0f8ac88de0d2affcf507112f87c526d2744a4 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Thu, 16 May 2024 13:15:47 -0700 Subject: [PATCH 1/3] exploration to abstract structure needed for span --- AutomergeUniffi/automerge.swift | 464 +++++++++++++++++- Sources/Automerge/Document.swift | 19 + .../_CAutomergeUniffi/include/automergeFFI.h | 36 +- Tests/AutomergeTests/TestBlocks.swift | 46 ++ rust/src/automerge.udl | 42 +- rust/src/doc.rs | 4 +- rust/src/lib.rs | 2 + rust/src/span.rs | 122 +++++ 8 files changed, 708 insertions(+), 27 deletions(-) create mode 100644 Tests/AutomergeTests/TestBlocks.swift create mode 100644 rust/src/span.rs diff --git a/AutomergeUniffi/automerge.swift b/AutomergeUniffi/automerge.swift index 2abb0dfc..b1090f1e 100644 --- a/AutomergeUniffi/automerge.swift +++ b/AutomergeUniffi/automerge.swift @@ -393,19 +393,6 @@ private struct FfiConverterUInt8: FfiConverterPrimitive { } } -private struct FfiConverterUInt32: FfiConverterPrimitive { - typealias FfiType = UInt32 - typealias SwiftType = UInt32 - - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt32 { - try lift(readInt(&buf)) - } - - public static func write(_ value: SwiftType, into buf: inout [UInt8]) { - writeInt(&buf, lower(value)) - } -} - private struct FfiConverterUInt64: FfiConverterPrimitive { typealias FfiType = UInt64 typealias SwiftType = UInt64 @@ -504,6 +491,105 @@ private struct FfiConverterString: FfiConverter { } } +public protocol AmValueProtocol: AnyObject {} + +open class AmValue: + AmValueProtocol +{ + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + /// This constructor can be used to instantiate a fake object. + /// - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that + /// may be implemented for classes extending [FFIObject]. + /// + /// - Warning: + /// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there + /// isn't a backing [Pointer] the FFI lower functions will crash. + public init(noPointer _: NoPointer) { + pointer = nil + } + + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + try! rustCall { uniffi_uniffi_automerge_fn_clone_amvalue(self.pointer, $0) } + } + + public convenience init(input: ScalarValue) { + let pointer = + try! rustCall { + uniffi_uniffi_automerge_fn_constructor_amvalue_new( + FfiConverterTypeScalarValue.lower(input), $0 + ) + } + self.init(unsafeFromRawPointer: pointer) + } + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_uniffi_automerge_fn_free_amvalue(pointer, $0) } + } + + public static func newFromMap(input: MapValue) -> AmValue { + try! FfiConverterTypeAMValue.lift(try! rustCall { + uniffi_uniffi_automerge_fn_constructor_amvalue_new_from_map( + FfiConverterTypeMapValue.lower(input), $0 + ) + }) + } +} + +public struct FfiConverterTypeAMValue: FfiConverter { + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = AmValue + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> AmValue { + AmValue(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: AmValue) -> UnsafeMutableRawPointer { + value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> AmValue { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if ptr == nil { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: AmValue, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + +public func FfiConverterTypeAMValue_lift(_ pointer: UnsafeMutableRawPointer) throws -> AmValue { + try FfiConverterTypeAMValue.lift(pointer) +} + +public func FfiConverterTypeAMValue_lower(_ value: AmValue) -> UnsafeMutableRawPointer { + FfiConverterTypeAMValue.lower(value) +} + public protocol DocProtocol: AnyObject { func actorId() -> ActorId @@ -567,7 +653,7 @@ public protocol DocProtocol: AnyObject { func insertObjectInList(obj: ObjId, index: UInt64, objType: ObjType) throws -> ObjId - func joinBlock(obj: ObjId, index: UInt32) throws + func joinBlock(obj: ObjId, index: UInt64) throws func length(obj: ObjId) -> UInt64 @@ -617,7 +703,7 @@ public protocol DocProtocol: AnyObject { func spliceText(obj: ObjId, start: UInt64, delete: Int64, chars: String) throws - func splitBlock(obj: ObjId, index: UInt32) throws -> ObjId + func splitBlock(obj: ObjId, index: UInt64) throws -> ObjId func text(obj: ObjId) throws -> String @@ -1027,11 +1113,11 @@ open class Doc: }) } - open func joinBlock(obj: ObjId, index: UInt32) throws { try rustCallWithError(FfiConverterTypeDocError.lift) { + open func joinBlock(obj: ObjId, index: UInt64) throws { try rustCallWithError(FfiConverterTypeDocError.lift) { uniffi_uniffi_automerge_fn_method_doc_join_block( self.uniffiClonePointer(), FfiConverterTypeObjId.lower(obj), - FfiConverterUInt32.lower(index), + FfiConverterUInt64.lower(index), $0 ) } @@ -1308,12 +1394,12 @@ open class Doc: } } - open func splitBlock(obj: ObjId, index: UInt32) throws -> ObjId { + open func splitBlock(obj: ObjId, index: UInt64) throws -> ObjId { try FfiConverterTypeObjId.lift(rustCallWithError(FfiConverterTypeDocError.lift) { uniffi_uniffi_automerge_fn_method_doc_split_block( self.uniffiClonePointer(), FfiConverterTypeObjId.lower(obj), - FfiConverterUInt32.lower(index), + FfiConverterUInt64.lower(index), $0 ) }) @@ -1682,6 +1768,87 @@ public func FfiConverterTypeKeyValue_lower(_ value: KeyValue) -> RustBuffer { FfiConverterTypeKeyValue.lower(value) } +public struct ListValue { + public var value: AmValue + public var marks: [String: ScalarValue] + public var conflict: Bool + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(value: AmValue, marks: [String: ScalarValue], conflict: Bool) { + self.value = value + self.marks = marks + self.conflict = conflict + } +} + +public struct FfiConverterTypeListValue: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ListValue { + try ListValue( + value: FfiConverterTypeAMValue.read(from: &buf), + marks: FfiConverterDictionaryStringTypeScalarValue.read(from: &buf), + conflict: FfiConverterBool.read(from: &buf) + ) + } + + public static func write(_ value: ListValue, into buf: inout [UInt8]) { + FfiConverterTypeAMValue.write(value.value, into: &buf) + FfiConverterDictionaryStringTypeScalarValue.write(value.marks, into: &buf) + FfiConverterBool.write(value.conflict, into: &buf) + } +} + +public func FfiConverterTypeListValue_lift(_ buf: RustBuffer) throws -> ListValue { + try FfiConverterTypeListValue.lift(buf) +} + +public func FfiConverterTypeListValue_lower(_ value: ListValue) -> RustBuffer { + FfiConverterTypeListValue.lower(value) +} + +public struct MapValue { + public var value: [String: ScalarValue] + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(value: [String: ScalarValue]) { + self.value = value + } +} + +extension MapValue: Equatable, Hashable { + public static func == (lhs: MapValue, rhs: MapValue) -> Bool { + if lhs.value != rhs.value { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(value) + } +} + +public struct FfiConverterTypeMapValue: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MapValue { + try MapValue( + value: FfiConverterDictionaryStringTypeScalarValue.read(from: &buf) + ) + } + + public static func write(_ value: MapValue, into buf: inout [UInt8]) { + FfiConverterDictionaryStringTypeScalarValue.write(value.value, into: &buf) + } +} + +public func FfiConverterTypeMapValue_lift(_ buf: RustBuffer) throws -> MapValue { + try FfiConverterTypeMapValue.lift(buf) +} + +public func FfiConverterTypeMapValue_lower(_ value: MapValue) -> RustBuffer { + FfiConverterTypeMapValue.lower(value) +} + public struct Mark { public var start: UInt64 public var end: UInt64 @@ -1749,6 +1916,49 @@ public func FfiConverterTypeMark_lower(_ value: Mark) -> RustBuffer { FfiConverterTypeMark.lower(value) } +public struct MarkSet { + public var marks: [String: ScalarValue] + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(marks: [String: ScalarValue]) { + self.marks = marks + } +} + +extension MarkSet: Equatable, Hashable { + public static func == (lhs: MarkSet, rhs: MarkSet) -> Bool { + if lhs.marks != rhs.marks { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(marks) + } +} + +public struct FfiConverterTypeMarkSet: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MarkSet { + try MarkSet( + marks: FfiConverterDictionaryStringTypeScalarValue.read(from: &buf) + ) + } + + public static func write(_ value: MarkSet, into buf: inout [UInt8]) { + FfiConverterDictionaryStringTypeScalarValue.write(value.marks, into: &buf) + } +} + +public func FfiConverterTypeMarkSet_lift(_ buf: RustBuffer) throws -> MarkSet { + try FfiConverterTypeMarkSet.lift(buf) +} + +public func FfiConverterTypeMarkSet_lower(_ value: MarkSet) -> RustBuffer { + FfiConverterTypeMarkSet.lower(value) +} + public struct Patch { public var path: [PathElement] public var action: PatchAction @@ -1851,6 +2061,112 @@ public func FfiConverterTypePathElement_lower(_ value: PathElement) -> RustBuffe FfiConverterTypePathElement.lower(value) } +public struct TextValue { + public var value: String + public var marks: [String: ScalarValue] + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(value: String, marks: [String: ScalarValue]) { + self.value = value + self.marks = marks + } +} + +extension TextValue: Equatable, Hashable { + public static func == (lhs: TextValue, rhs: TextValue) -> Bool { + if lhs.value != rhs.value { + return false + } + if lhs.marks != rhs.marks { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(value) + hasher.combine(marks) + } +} + +public struct FfiConverterTypeTextValue: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TextValue { + try TextValue( + value: FfiConverterString.read(from: &buf), + marks: FfiConverterDictionaryStringTypeScalarValue.read(from: &buf) + ) + } + + public static func write(_ value: TextValue, into buf: inout [UInt8]) { + FfiConverterString.write(value.value, into: &buf) + FfiConverterDictionaryStringTypeScalarValue.write(value.marks, into: &buf) + } +} + +public func FfiConverterTypeTextValue_lift(_ buf: RustBuffer) throws -> TextValue { + try FfiConverterTypeTextValue.lift(buf) +} + +public func FfiConverterTypeTextValue_lower(_ value: TextValue) -> RustBuffer { + FfiConverterTypeTextValue.lower(value) +} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum AmValueType { + case scalar + case list + case map + case text +} + +public struct FfiConverterTypeAMValueType: FfiConverterRustBuffer { + typealias SwiftType = AmValueType + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> AmValueType { + let variant: Int32 = try readInt(&buf) + switch variant { + case 1: return .scalar + + case 2: return .list + + case 3: return .map + + case 4: return .text + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: AmValueType, into buf: inout [UInt8]) { + switch value { + case .scalar: + writeInt(&buf, Int32(1)) + + case .list: + writeInt(&buf, Int32(2)) + + case .map: + writeInt(&buf, Int32(3)) + + case .text: + writeInt(&buf, Int32(4)) + } + } +} + +public func FfiConverterTypeAMValueType_lift(_ buf: RustBuffer) throws -> AmValueType { + try FfiConverterTypeAMValueType.lift(buf) +} + +public func FfiConverterTypeAMValueType_lower(_ value: AmValueType) -> RustBuffer { + FfiConverterTypeAMValueType.lower(value) +} + +extension AmValueType: Equatable, Hashable {} + public enum DecodeSyncStateError { case Internal(message: String) } @@ -2500,6 +2816,62 @@ extension ScalarValue: Equatable, Hashable {} // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. +public enum Span { + case text( + text: String, + marks: MarkSet? + ) + case block( + value: MapValue + ) +} + +public struct FfiConverterTypeSpan: FfiConverterRustBuffer { + typealias SwiftType = Span + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Span { + let variant: Int32 = try readInt(&buf) + switch variant { + case 1: return try .text( + text: FfiConverterString.read(from: &buf), + marks: FfiConverterOptionTypeMarkSet.read(from: &buf) + ) + + case 2: return try .block( + value: FfiConverterTypeMapValue.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: Span, into buf: inout [UInt8]) { + switch value { + case let .text(text, marks): + writeInt(&buf, Int32(1)) + FfiConverterString.write(text, into: &buf) + FfiConverterOptionTypeMarkSet.write(marks, into: &buf) + + case let .block(value): + writeInt(&buf, Int32(2)) + FfiConverterTypeMapValue.write(value, into: &buf) + } + } +} + +public func FfiConverterTypeSpan_lift(_ buf: RustBuffer) throws -> Span { + try FfiConverterTypeSpan.lift(buf) +} + +public func FfiConverterTypeSpan_lower(_ value: Span) -> RustBuffer { + FfiConverterTypeSpan.lower(value) +} + +extension Span: Equatable, Hashable {} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + public enum Value { case object( typ: ObjType, @@ -2595,6 +2967,27 @@ private struct FfiConverterOptionTypeChange: FfiConverterRustBuffer { } } +private struct FfiConverterOptionTypeMarkSet: FfiConverterRustBuffer { + typealias SwiftType = MarkSet? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeMarkSet.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeMarkSet.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + private struct FfiConverterOptionTypeValue: FfiConverterRustBuffer { typealias SwiftType = Value? @@ -2856,6 +3249,29 @@ private struct FfiConverterSequenceTypeChangeHash: FfiConverterRustBuffer { } } +private struct FfiConverterDictionaryStringTypeScalarValue: FfiConverterRustBuffer { + public static func write(_ value: [String: ScalarValue], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for (key, value) in value { + FfiConverterString.write(key, into: &buf) + FfiConverterTypeScalarValue.write(value, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [String: ScalarValue] { + let len: Int32 = try readInt(&buf) + var dict = [String: ScalarValue]() + dict.reserveCapacity(Int(len)) + for _ in 0 ..< len { + let key = try FfiConverterString.read(from: &buf) + let value = try FfiConverterTypeScalarValue.read(from: &buf) + dict[key] = value + } + return dict + } +} + private struct FfiConverterDictionaryStringTypeValue: FfiConverterRustBuffer { public static func write(_ value: [String: Value], into buf: inout [UInt8]) { let len = Int32(value.count) @@ -3123,7 +3539,7 @@ private var initializationResult: InitializationResult { if uniffi_uniffi_automerge_checksum_method_doc_insert_object_in_list() != 30538 { return InitializationResult.apiChecksumMismatch } - if uniffi_uniffi_automerge_checksum_method_doc_join_block() != 37348 { + if uniffi_uniffi_automerge_checksum_method_doc_join_block() != 8448 { return InitializationResult.apiChecksumMismatch } if uniffi_uniffi_automerge_checksum_method_doc_length() != 30352 { @@ -3198,7 +3614,7 @@ private var initializationResult: InitializationResult { if uniffi_uniffi_automerge_checksum_method_doc_splice_text() != 20602 { return InitializationResult.apiChecksumMismatch } - if uniffi_uniffi_automerge_checksum_method_doc_split_block() != 10956 { + if uniffi_uniffi_automerge_checksum_method_doc_split_block() != 15883 { return InitializationResult.apiChecksumMismatch } if uniffi_uniffi_automerge_checksum_method_doc_text() != 64716 { @@ -3225,6 +3641,12 @@ private var initializationResult: InitializationResult { if uniffi_uniffi_automerge_checksum_method_syncstate_their_heads() != 39870 { return InitializationResult.apiChecksumMismatch } + if uniffi_uniffi_automerge_checksum_constructor_amvalue_new() != 11832 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_uniffi_automerge_checksum_constructor_amvalue_new_from_map() != 44769 { + return InitializationResult.apiChecksumMismatch + } if uniffi_uniffi_automerge_checksum_constructor_doc_load() != 20048 { return InitializationResult.apiChecksumMismatch } diff --git a/Sources/Automerge/Document.swift b/Sources/Automerge/Document.swift index 58fa1b8b..c705f005 100644 --- a/Sources/Automerge/Document.swift +++ b/Sources/Automerge/Document.swift @@ -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: diff --git a/Sources/_CAutomergeUniffi/include/automergeFFI.h b/Sources/_CAutomergeUniffi/include/automergeFFI.h index d521cb4f..eda61352 100644 --- a/Sources/_CAutomergeUniffi/include/automergeFFI.h +++ b/Sources/_CAutomergeUniffi/include/automergeFFI.h @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/Tests/AutomergeTests/TestBlocks.swift b/Tests/AutomergeTests/TestBlocks.swift new file mode 100644 index 00000000..2d419365 --- /dev/null +++ b/Tests/AutomergeTests/TestBlocks.swift @@ -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) + } +} diff --git a/rust/src/automerge.udl b/rust/src/automerge.udl index 76b24df9..ff8402d0 100644 --- a/rust/src/automerge.udl +++ b/rust/src/automerge.udl @@ -104,6 +104,44 @@ dictionary Mark { ScalarValue value; }; +dictionary MarkSet { + record marks; +}; + +enum AMValueType { + "Scalar", + "List", + "Map", + "Text", +}; + +interface AMValue { + constructor(ScalarValue input); + [Name=new_from_map] + constructor(MapValue input); +}; + +dictionary MapValue { + record value; +}; + +dictionary TextValue { + string value; + record marks; +}; + +dictionary ListValue { + AMValue value; + record marks; + boolean conflict; +}; + +[Enum] +interface Span { + Text ( string text, MarkSet? marks ); + Block ( MapValue value ); +}; + dictionary PathElement { Prop prop; ObjId obj; @@ -180,9 +218,9 @@ interface Doc { sequence marks_at_position(ObjId obj, Position position, sequence 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); diff --git a/rust/src/doc.rs b/rust/src/doc.rs index e115e5b8..6f4b802b 100644 --- a/rust/src/doc.rs +++ b/rust/src/doc.rs @@ -500,14 +500,14 @@ impl Doc { Ok(Mark::from_markset(markset, index as u64)) } - pub fn split_block(&self, obj: ObjId, index: u32) -> Result { + pub fn split_block(&self, obj: ObjId, index: u64) -> Result { 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())?; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index a0f14a8b..c8ce30ea 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -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}; diff --git a/rust/src/span.rs b/rust/src/span.rs new file mode 100644 index 00000000..3e97630b --- /dev/null +++ b/rust/src/span.rs @@ -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, // as Vec to convert through hydrate? +// pub r#type: String, +// pub attr: HashMap, // 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, + }, + /// 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, +} + +// 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 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) 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, + map_value: Option, + list_value: Option>, + text_value: Option, +} + +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, +} + +// loosely maps to am::hydrate::ListValue +// need to create From<> in to convert over +pub struct ListValue { + pub value: Arc, + pub marks: HashMap, + 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, +} From 3a405dbd8fc27b61624865135d3ffc4ed0498373 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Tue, 18 Jun 2024 08:58:51 -0700 Subject: [PATCH 2/3] indirect enum equivalent - paired w/ Alex --- rust/src/automerge.udl | 25 ++++------- rust/src/lib.rs | 2 +- rust/src/span.rs | 97 ++++-------------------------------------- 3 files changed, 18 insertions(+), 106 deletions(-) diff --git a/rust/src/automerge.udl b/rust/src/automerge.udl index ff8402d0..620239ab 100644 --- a/rust/src/automerge.udl +++ b/rust/src/automerge.udl @@ -108,29 +108,20 @@ dictionary MarkSet { record marks; }; -enum AMValueType { - "Scalar", - "List", - "Map", - "Text", -}; - +[Enum] interface AMValue { - constructor(ScalarValue input); - [Name=new_from_map] - constructor(MapValue input); -}; - -dictionary MapValue { - record value; + Map ( record value ); + Scalar ( ScalarValue value ); + List ( sequence value ); + Text ( HydratedText value ); }; -dictionary TextValue { +dictionary HydratedText { string value; record marks; }; -dictionary ListValue { +dictionary HydratedList { AMValue value; record marks; boolean conflict; @@ -139,7 +130,7 @@ dictionary ListValue { [Enum] interface Span { Text ( string text, MarkSet? marks ); - Block ( MapValue value ); + Block ( record value ); }; dictionary PathElement { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index c8ce30ea..80266aa3 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -27,4 +27,4 @@ use sync_state::{DecodeSyncStateError, SyncState}; mod value; use value::Value; mod span; -use span::{AMValue, AMValueType, ListValue, MapValue, MarkSet, Span, TextValue}; +use span::{AMValue, HydratedList, HydratedText, MarkSet, Span}; diff --git a/rust/src/span.rs b/rust/src/span.rs index 3e97630b..47bf7d9e 100644 --- a/rust/src/span.rs +++ b/rust/src/span.rs @@ -1,17 +1,7 @@ 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, // as Vec to convert through hydrate? -// pub r#type: String, -// pub attr: HashMap, // as MapValue -// } - // maps to am::iter::Span // need to create From<> in to convert over pub enum Span { @@ -21,7 +11,7 @@ pub enum Span { marks: Option, }, /// A block marker - Block { value: MapValue }, + Block { value: HashMap }, } // loosely maps to am::marks:MarkSet @@ -30,93 +20,24 @@ pub struct MarkSet { pub marks: HashMap, } -// 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 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) 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, - map_value: Option, - list_value: Option>, - text_value: Option, -} - -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, +pub enum AMValue { + Map { value: HashMap }, + Scalar { value: ScalarValue }, + List { value: Vec }, + Text { value: HydratedText }, } // loosely maps to am::hydrate::ListValue // need to create From<> in to convert over -pub struct ListValue { - pub value: Arc, +pub struct HydratedList { + pub value: AMValue, pub marks: HashMap, pub conflict: bool, } // loosely maps to am::hydrate::Text // need to create From<> in to convert over -pub struct TextValue { +pub struct HydratedText { pub value: String, pub marks: HashMap, } From acc1388770702e14e191abd9421ea7d7c1304987 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Fri, 28 Jun 2024 10:22:31 -0700 Subject: [PATCH 3/3] wip - from functions --- rust/src/automerge.udl | 4 ++-- rust/src/doc.rs | 10 ++++++++++ rust/src/lib.rs | 2 +- rust/src/span.rs | 41 ++++++++++++++++++++++++++++++++++++++--- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/rust/src/automerge.udl b/rust/src/automerge.udl index 620239ab..63d12a46 100644 --- a/rust/src/automerge.udl +++ b/rust/src/automerge.udl @@ -112,7 +112,7 @@ dictionary MarkSet { interface AMValue { Map ( record value ); Scalar ( ScalarValue value ); - List ( sequence value ); + List ( sequence value ); Text ( HydratedText value ); }; @@ -121,7 +121,7 @@ dictionary HydratedText { record marks; }; -dictionary HydratedList { +dictionary HydratedListItem { AMValue value; record marks; boolean conflict; diff --git a/rust/src/doc.rs b/rust/src/doc.rs index 6f4b802b..dad8e7b0 100644 --- a/rust/src/doc.rs +++ b/rust/src/doc.rs @@ -7,6 +7,7 @@ use automerge::{transaction::Transactable, ReadDoc}; use crate::actor_id::ActorId; use crate::cursor::Position; use crate::mark::{ExpandMark, KeyValue, Mark}; +use crate::span::{Span}; use crate::patches::Patch; use crate::{ Change, ChangeHash, Cursor, ObjId, ObjType, PathElement, ScalarValue, SyncState, Value, @@ -514,6 +515,15 @@ impl Doc { Ok(()) } + pub fn spans(&self, obj: ObjId) -> Result, DocError> { + let mut doc = self.0.write().unwrap(); + let obj = am::ObjId::from(obj); + let x = doc.spans(obj).unwrap(); + let y = x + .into_iter() + .map(am::iter::spans::from); + } + pub fn merge(&self, other: Arc) -> Result<(), DocError> { let mut doc = self.0.write().unwrap(); let mut other = other.0.write().unwrap(); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 80266aa3..ca0ebb41 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -27,4 +27,4 @@ use sync_state::{DecodeSyncStateError, SyncState}; mod value; use value::Value; mod span; -use span::{AMValue, HydratedList, HydratedText, MarkSet, Span}; +use span::{AMValue, HydratedListItem, HydratedText, MarkSet, Span}; diff --git a/rust/src/span.rs b/rust/src/span.rs index 47bf7d9e..27357cf3 100644 --- a/rust/src/span.rs +++ b/rust/src/span.rs @@ -1,6 +1,6 @@ use crate::ScalarValue; use std::collections::HashMap; -// use automerge as am; +use automerge as am; // maps to am::iter::Span // need to create From<> in to convert over @@ -14,22 +14,57 @@ pub enum Span { Block { value: HashMap }, } +// impl From for am::iter::Spans<'_> { +// fn from(value: Span) -> Self { +// let inner: [u8; 32] = value.0.try_into().unwrap(); +// am::ChangeHash(inner) +// } +// } + +impl<'a> From<&'a am::iter::Span> for Span { + fn from(value: &'a am::iter::Span) -> Self { + match value { + am::iter::Span::Text( t, m) => Self::Text { text: t.to_string(), marks: Option>::from() }, + am::iter::Span::Block( value ) => Self::Block { value: HashMap::from(value) } + } + } +} + +impl<'a> From<&'a am::hydrate::Map> for HashMap { + fn from(value: &'a am::hydrate::Map) -> Self { + let mut new_hash_map:HashMap = HashMap::new(); + // fill in the middle bits... + return new_hash_map; + } +} + // loosely maps to am::marks:MarkSet // need to create From<> in to convert over pub struct MarkSet { pub marks: HashMap, } +impl<'a> From<&'a am::marks::MarkSet> for MarkSet { + fn from(value: &'a am::marks::MarkSet) -> Self { + let mut new_hash:HashMap = HashMap::new(); + // iterate through MarkSet, building a hashmap for this MarkSet + for (k, v) in value.iter() { + new_hash.insert(k.to_string(), v.into()); + } + Self { marks: new_hash } + } +} + pub enum AMValue { Map { value: HashMap }, Scalar { value: ScalarValue }, - List { value: Vec }, + List { value: Vec }, Text { value: HydratedText }, } // loosely maps to am::hydrate::ListValue // need to create From<> in to convert over -pub struct HydratedList { +pub struct HydratedListItem { pub value: AMValue, pub marks: HashMap, pub conflict: bool,