diff --git a/Sources/Automerge/Automerge.docc/Automerge.md b/Sources/Automerge/Automerge.docc/Automerge.md index 5af010e5..5e0279c6 100644 --- a/Sources/Automerge/Automerge.docc/Automerge.md +++ b/Sources/Automerge/Automerge.docc/Automerge.md @@ -68,6 +68,7 @@ Read to get a quick taste of how to use Automerge, or - ``Automerge/AutomergeText`` - ``Automerge/Cursor`` +- ``Automerge/Position`` - ``Automerge/Mark`` - ``Automerge/ExpandMark`` @@ -111,6 +112,7 @@ Read to get a quick taste of how to use Automerge, or ### Type Conversion Errors +- ``Automerge/ScalarValueConversionError`` - ``Automerge/BooleanScalarConversionError`` - ``Automerge/BytesScalarConversionError`` - ``Automerge/IntScalarConversionError`` diff --git a/Sources/Automerge/Automerge.docc/Curation/Codable/AutomergeEncoder.md b/Sources/Automerge/Automerge.docc/Curation/Codable/AutomergeEncoder.md index 9274b40d..0ad6d75b 100644 --- a/Sources/Automerge/Automerge.docc/Curation/Codable/AutomergeEncoder.md +++ b/Sources/Automerge/Automerge.docc/Curation/Codable/AutomergeEncoder.md @@ -17,6 +17,9 @@ var myColors = ColorList(colors: ["blue", "red"]) try encoder.encode(myColors) ``` +To support cross-platform usage, when provided a optional type to encode, the encoder writes a +``ScalarValue/Null`` into the Document as opposed to not creating the relevant entry in a map or list. + ## Topics ### Creating an Encoder diff --git a/Sources/Automerge/Automerge.docc/Curation/Document.md b/Sources/Automerge/Automerge.docc/Curation/Document.md index 51e26e4b..75b6d4e2 100644 --- a/Sources/Automerge/Automerge.docc/Curation/Document.md +++ b/Sources/Automerge/Automerge.docc/Curation/Document.md @@ -125,6 +125,7 @@ ### Observing Documents - ``objectWillChange`` +- ``objectDidChange`` ### Transfering Documents diff --git a/Sources/Automerge/BoundTypes/AutomergeText.swift b/Sources/Automerge/BoundTypes/AutomergeText.swift index cea3bad0..95e13b52 100644 --- a/Sources/Automerge/BoundTypes/AutomergeText.swift +++ b/Sources/Automerge/BoundTypes/AutomergeText.swift @@ -224,7 +224,7 @@ public final class AutomergeText: Codable, @unchecked Sendable { /// document. /// - Parameters: /// - doc: The Automerge document associated with this reference. - /// - path: A string path that represents a `Text` container within the Automerge document. + /// - id: The Automerge object ID of the text object to bind. public func bind(doc: Document, id: ObjId) throws { // this assert runs afoul of the encoder, which doesn't make sense right now, but // I don't want to second guess it at the moment. diff --git a/Sources/Automerge/ChangeHash.swift b/Sources/Automerge/ChangeHash.swift index f839f4ff..5da89333 100644 --- a/Sources/Automerge/ChangeHash.swift +++ b/Sources/Automerge/ChangeHash.swift @@ -12,7 +12,6 @@ public struct ChangeHash: Equatable, Hashable, CustomDebugStringConvertible, Sen } public extension Set { - /// Transforms each `ChangeHash` in the set into its byte array (`[UInt8]`). This raw byte representation /// captures the state of the document at a specific point in its history, allowing for efficient storage /// and retrieval of document states. @@ -25,16 +24,16 @@ public extension Set { } public extension Data { - - /// Interprets the data to return the data as a set of change hashes that represent a state within an Automerge document. If the data is not a multiple of 32 bytes, returns nil. + /// Interprets the data to return the data as a set of change hashes that represent a state within an Automerge + /// document. If the data is not a multiple of 32 bytes, returns nil. func heads() -> Set? { let rawBytes: [UInt8] = Array(self) guard rawBytes.count % 32 == 0 else { return nil } let totalHashes = rawBytes.count / 32 - let heads = (0..(_: T.Type) throws -> T { if T.self == AutomergeText.self { // Special case decoding AutomergeText - when it's the top level type being encoded, @@ -37,8 +37,8 @@ public struct AutomergeDecoder { /// Returns the type you specify, decoded from the Automerge document referenced by the decoder. /// - Parameters: - /// - _: _ The type of the value to decode from the Automerge document. - /// - path: The path to the schema location within the Automerge document to attempt to decode into the type you + /// - : The type of the value to decode from the Automerge document. + /// - path: The path to the schema location within the Automerge document to attempt to decode into the type you /// provide. /// /// The `path` parameter accepts any type conforming to the `CodingKey` protocol. diff --git a/Sources/Automerge/Codable/Decoding/AutomergeSingleValueDecodingContainer.swift b/Sources/Automerge/Codable/Decoding/AutomergeSingleValueDecodingContainer.swift index ce1642d6..c1195b93 100644 --- a/Sources/Automerge/Codable/Decoding/AutomergeSingleValueDecodingContainer.swift +++ b/Sources/Automerge/Codable/Decoding/AutomergeSingleValueDecodingContainer.swift @@ -184,7 +184,6 @@ struct AutomergeSingleValueDecodingContainer: SingleValueDecodingContainer { debugDescription: "Expected to decode \(T.self) from \(value), but it wasn't `.text`." )) } - default: return try T(from: impl) } diff --git a/Sources/Automerge/Codable/Encoding/AutomergeEncoder.swift b/Sources/Automerge/Codable/Encoding/AutomergeEncoder.swift index 8acae850..6e2c14b0 100644 --- a/Sources/Automerge/Codable/Encoding/AutomergeEncoder.swift +++ b/Sources/Automerge/Codable/Encoding/AutomergeEncoder.swift @@ -1,4 +1,4 @@ -/// An encoder that stores types that conform to the codable protocol into an Automerge document. +/// An encoder that stores types that conform to the Codable protocol into an Automerge document. public struct AutomergeEncoder { /// The user info dictionary for the encoder. public var userInfo: [CodingUserInfoKey: Any] = [:] diff --git a/Sources/Automerge/Cursor.swift b/Sources/Automerge/Cursor.swift index 4a6ac223..1b84fd15 100644 --- a/Sources/Automerge/Cursor.swift +++ b/Sources/Automerge/Cursor.swift @@ -33,9 +33,9 @@ public enum Position { extension Position { func toFfi() -> FfiPosition { switch self { - case .cursor(let cursor): + case let .cursor(cursor): return .cursor(position: cursor.bytes) - case .index(let index): + case let .index(index): return .index(position: index) } } diff --git a/Sources/Automerge/Document.swift b/Sources/Automerge/Document.swift index e0103cae..2ac06a36 100644 --- a/Sources/Automerge/Document.swift +++ b/Sources/Automerge/Document.swift @@ -27,9 +27,13 @@ public final class Document: @unchecked Sendable { try work() } #endif - + #if canImport(Combine) - public let objectDidChange: PassthroughSubject<(), Never> = .init() + /// A publisher that sends a signal after the document is updated. + /// + /// You can use the signal from this publisher to read the and record ``Document/heads()`` + /// to get the state indicator of the document after the change is complete. + public let objectDidChange: PassthroughSubject = .init() #endif var reportingLogLevel: LogVerbosity @@ -129,7 +133,7 @@ public final class Document: @unchecked Sendable { /// - ty: The type of object to add to the dictionary. /// - Returns: The object Id that references the object added. public func putObject(obj: ObjId, key: String, ty: ObjType) throws -> ObjId { - return try lock { + try lock { sendObjectWillChange() defer { sendObjectDidChange() } return try self.doc.wrapErrors { @@ -149,7 +153,7 @@ public final class Document: @unchecked Sendable { /// If the index position doesn't yet exist within the array, this method will throw an error. /// To add an object that extends the array, use the method ``insertObject(obj:index:ty:)``. public func putObject(obj: ObjId, index: UInt64, ty: ObjType) throws -> ObjId { - return try lock { + try lock { sendObjectWillChange() defer { sendObjectDidChange() } return try self.doc.wrapErrors { @@ -186,7 +190,7 @@ public final class Document: @unchecked Sendable { /// If you want to change an existing index, use the ``putObject(obj:index:ty:)`` to put in an object or /// ``put(obj:index:value:)`` to put in a value. public func insertObject(obj: ObjId, index: UInt64, ty: ObjType) throws -> ObjId { - return try lock { + try lock { sendObjectWillChange() defer { sendObjectDidChange() } return try self.doc.wrapErrors { @@ -558,7 +562,7 @@ public final class Document: @unchecked Sendable { /// - position: The index position in the list, or index of the UTF-8 view in the string for a text object. /// - Returns: A cursor that references the position you specified. public func cursor(obj: ObjId, position: UInt64) throws -> Cursor { - return try lock { + try lock { sendObjectWillChange() defer { sendObjectDidChange() } return try Cursor(bytes: self.doc.wrapErrors { try $0.cursor(obj: obj.bytes, position: position) }) @@ -573,7 +577,7 @@ public final class Document: @unchecked Sendable { /// - heads: The set of ``ChangeHash`` that represents a point of time in the history the document. /// - Returns: A cursor that references the position and point in time you specified. public func cursorAt(obj: ObjId, position: UInt64, heads: Set) throws -> Cursor { - return try lock { + try lock { sendObjectWillChange() defer { sendObjectDidChange() } return try Cursor(bytes: self.doc.wrapErrors { try $0.cursorAt( @@ -641,7 +645,7 @@ public final class Document: @unchecked Sendable { /// deleting. /// - delete: The number of unicode scalars to delete from the `start` index. /// If negative, the function deletes characters preceding `start` index, rather than following it. - /// - values: The characters to insert after the `start` index. + /// - value: The characters to insert after the `start` index. /// /// With `spliceText`, the `start` and `delete` parameters represent integer distances of unicode scalars of the /// Swift strings, not the counts of Characters (or grapheme clusters). @@ -797,7 +801,8 @@ public final class Document: @unchecked Sendable { /// /// - Parameters: /// - obj: The identifier of the text object, represented by an ``ObjId``. - /// - position: The position within the text, represented by a ``Position`` enum which can be a ``Cursor`` or an `UInt64` as a fixed position. + /// - position: The position within the text, represented by a ``Position`` enum which can be a ``Cursor`` or an + /// `UInt64` as a fixed position. /// - heads: A set of `ChangeHash` values that represents a point in time in the document's history. /// - Returns: An array of `Mark` objects for the text object at the specified position. /// @@ -811,16 +816,22 @@ public final class Document: @unchecked Sendable { /// ``` /// /// ## Recommendation + /// /// Use this method to query the marks applied to a text object at a specific position. - /// This can be useful for retrieving ``Marks`` related to a character without traversing the full document. + /// This can be useful for retrieving the list of ``Automerge/Mark`` related to a character without + /// traversing the full document. /// /// ## When to Use Cursor vs. Index /// /// While you can specify the position either with a `Cursor` or an `Index`, there are important distinctions: /// - /// - **Cursor**: Use a `Cursor` when you need to track a position that might change over time due to edits in the text object. A `Cursor` provides a way to maintain a reference to a logical position within the text even if the text content changes, making it more robust in collaborative or frequently edited documents. + /// - **Cursor**: Use a `Cursor` when you need to track a position that might change over time due to edits in the + /// text object. A `Cursor` provides a way to maintain a reference to a logical position within the text even if the + /// text content changes, making it more robust in collaborative or frequently edited documents. /// - /// - **Index**: Use an `Index` when you have a fixed position and you are sure that the text content will not change, or changes are irrelevant to your current operation. An index is a straightforward approach for static text content. + /// - **Index**: Use an `Index` when you have a fixed position and you are sure that the text content will not + /// change, or changes are irrelevant to your current operation. An index is a straightforward approach for static + /// text content. /// /// # See Also /// ``marksAt(obj:position:)`` @@ -845,7 +856,8 @@ public final class Document: @unchecked Sendable { /// /// - Parameters: /// - obj: The identifier of the text object, represented by an ``ObjId``. - /// - position: The position within the text, represented by a ``Position`` enum which can be a ``Cursor`` or an `UInt64` as a fixed position. + /// - position: The position within the text, represented by a ``Position`` enum which can be a ``Cursor`` or an + /// `UInt64` as a fixed position. /// - Returns: An array of `Mark` objects for the text object at the specified position. /// - Note: This method retrieves marks from the latest version of the document. /// If you need to specify a point in the document's history, refer to ``marksAt(obj:position:heads:)``. @@ -861,15 +873,20 @@ public final class Document: @unchecked Sendable { /// /// ## Recommendation /// Use this method to query the marks applied to a text object at a specific position. - /// This can be useful for retrieving ``Marks`` related to a character without traversing the full document. + /// This can be useful for retrieving the list of ``Automerge/Mark`` related to a character without + /// traversing the full document. /// /// ## When to Use Cursor vs. Index /// /// While you can specify the position either with a `Cursor` or an `Index`, there are important distinctions: /// - /// - **Cursor**: Use a `Cursor` when you need to track a position that might change over time due to edits in the text object. A `Cursor` provides a way to maintain a reference to a logical position within the text even if the text content changes, making it more robust in collaborative or frequently edited documents. + /// - **Cursor**: Use a `Cursor` when you need to track a position that might change over time due to edits in the + /// text object. A `Cursor` provides a way to maintain a reference to a logical position within the text even if the + /// text content changes, making it more robust in collaborative or frequently edited documents. /// - /// - **Index**: Use an `Index` when you have a fixed position and you are sure that the text content will not change, or changes are irrelevant to your current operation. An index is a straightforward approach for static text content. + /// - **Index**: Use an `Index` when you have a fixed position and you are sure that the text content will not + /// change, or changes are irrelevant to your current operation. An index is a straightforward approach for static + /// text content. /// /// # See Also /// ``marksAt(obj:position:heads:)`` @@ -901,7 +918,7 @@ public final class Document: @unchecked Sendable { /// The `save` function also compacts the memory footprint of an Automerge document and increments the result of /// ``heads()``, which indicates a specific point in time for the history of the document. public func save() -> Data { - return lock { + lock { sendObjectWillChange() defer { sendObjectDidChange() } return self.doc.wrapErrors { @@ -956,7 +973,7 @@ public final class Document: @unchecked Sendable { /// - message: The message from the peer to update this document and sync state. /// - Returns: An array of ``Patch`` that represent the changes applied from the peer. public func receiveSyncMessageWithPatches(state: SyncState, message: Data) throws -> [Patch] { - return try lock { + try lock { sendObjectWillChange() defer { sendObjectDidChange() } let patches = try self.doc.wrapErrors { @@ -1007,7 +1024,7 @@ public final class Document: @unchecked Sendable { /// - Parameter other: another ``Document`` /// - Returns: A list of ``Patch`` the represent the changes applied when merging the other document. public func mergeWithPatches(other: Document) throws -> [Patch] { - return try lock { + try lock { sendObjectWillChange() defer { sendObjectDidChange() } let patches = try self.doc.wrapErrorsWithOther(other: other.doc) { @@ -1078,8 +1095,8 @@ public final class Document: @unchecked Sendable { /// ``` /// /// - Parameters: - /// - from: The set of heads at beginning point in the documents history. - /// - to: The set of heads at ending point in the documents history. + /// - before: The set of heads at beginning point in the documents history. + /// - after: The set of heads at ending point in the documents history. /// - Note: `from` and `to` do not have to be chronological. Document state can move backward. /// - Returns: The difference needed to produce a document at `to` when it is set at `from` in history. public func difference(from before: Set, to after: Set) -> [Patch] { @@ -1100,7 +1117,7 @@ public final class Document: @unchecked Sendable { /// ``` /// /// - Parameters: - /// - since: The set of heads at the point in the documents history to compare to. + /// - lhs: The set of heads at the point in the documents history to compare to. /// - Returns: The difference needed to produce current document given an arbitrary /// point in the history. public func difference(since lhs: Set) -> [Patch] { @@ -1116,7 +1133,7 @@ public final class Document: @unchecked Sendable { /// ``` /// /// - Parameters: - /// - to: The set of heads at ending point in the documents history. + /// - rhs: The set of heads at ending point in the documents history. /// - Returns: The difference needed to move current document to a previous point /// in the history. public func difference(to rhs: Set) -> [Patch] { @@ -1186,7 +1203,7 @@ public final class Document: @unchecked Sendable { /// ``encodeNewChanges()``, ``encodeChangesSince(heads:)`` or any /// concatenation of those. public func applyEncodedChangesWithPatches(encoded: Data) throws -> [Patch] { - return try lock { + try lock { sendObjectWillChange() defer { sendObjectDidChange() } let patches = try self.doc.wrapErrors { @@ -1253,6 +1270,7 @@ extension Document: ObservableObject { // #endif objectWillChange.send() } + fileprivate func sendObjectDidChange() { objectDidChange.send() } diff --git a/Tests/AutomergeTests/TestChanges.swift b/Tests/AutomergeTests/TestChanges.swift index 59fcfc07..a6ee7ee3 100644 --- a/Tests/AutomergeTests/TestChanges.swift +++ b/Tests/AutomergeTests/TestChanges.swift @@ -113,7 +113,7 @@ class ChangeSetTests: XCTestCase { try doc.merge(other: doc2) try doc.merge(other: doc3) - let rawHashes = (0..<500).map { _ in doc.heads().raw() } + let rawHashes = (0 ..< 500).map { _ in doc.heads().raw() } XCTAssertEqual(Set(rawHashes).count, 1) } diff --git a/Tests/AutomergeTests/TestMarks.swift b/Tests/AutomergeTests/TestMarks.swift index fbceb8f0..aef0a39b 100644 --- a/Tests/AutomergeTests/TestMarks.swift +++ b/Tests/AutomergeTests/TestMarks.swift @@ -77,7 +77,7 @@ class MarksTestCase: XCTestCase { XCTAssertEqual(marks, [ Mark(start: 2, end: 2, name: "bold", value: .Boolean(true)), - Mark(start: 2, end: 2, name: "italic", value: .Boolean(true)) + Mark(start: 2, end: 2, name: "italic", value: .Boolean(true)), ]) } @@ -93,7 +93,7 @@ class MarksTestCase: XCTestCase { XCTAssertEqual(marks, [ Mark(start: 2, end: 2, name: "bold", value: .Boolean(true)), - Mark(start: 2, end: 2, name: "italic", value: .Boolean(true)) + Mark(start: 2, end: 2, name: "italic", value: .Boolean(true)), ]) } } diff --git a/Tests/AutomergeTests/TestObservableDocument.swift b/Tests/AutomergeTests/TestObservableDocument.swift index b043cb15..5ca405be 100644 --- a/Tests/AutomergeTests/TestObservableDocument.swift +++ b/Tests/AutomergeTests/TestObservableDocument.swift @@ -45,7 +45,7 @@ class ObservableDocumentTestCase: XCTestCase { stashedHeads = doc.heads() } XCTAssertNotNil(willChangeHandle) - + let didChangeHandle = doc.objectDidChange.sink { _ = doc.heads() }