Skip to content

Commit

Permalink
exposing Automerge::get_marks through to Swift (#186)
Browse files Browse the repository at this point in the history
* rust: implement get_marks at given position/cursor
* swift: implement get_marks at given position/cursor
* unit test: get_marks at given position/cursor
* doc: curation::marksAt given position/cursor
* Update Sources/Automerge/Document.swift
Co-authored-by: Joseph Heck <heckj@mac.com>
* comments: unified implementation of marks at position
* add missing links into documentation
* minor adjustments for binding `marksAt` from swift to rust
* comments: add detailed information of use for `marksAt`
* minor: reduce verbosity of the api doc for marksAt
* removing unnecessary public accessibility to the cursor

---------

Co-authored-by: Joseph Heck <heckj@mac.com>
  • Loading branch information
miguelangel-dev and heckj authored Jun 14, 2024
1 parent d12280d commit 76e59a3
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 11 deletions.
2 changes: 2 additions & 0 deletions Sources/Automerge/Automerge.docc/Curation/Document.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
- ``text(obj:)``
- ``length(obj:)``
- ``marks(obj:)``
- ``marksAt(obj:position:)``

### Updating Text values

Expand Down Expand Up @@ -100,6 +101,7 @@
- ``textAt(obj:heads:)``
- ``lengthAt(obj:heads:)``
- ``marksAt(obj:heads:)``
- ``marksAt(obj:position:heads:)``

### Saving, forking, and merging documents

Expand Down
1 change: 1 addition & 0 deletions Sources/Automerge/Automerge.docc/ModelingData.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ See the documentation for ``Document`` for more detail on the individual methods
- ``Automerge/Document/text(obj:)``
- ``Automerge/Document/length(obj:)``
- ``Automerge/Document/marks(obj:)``
- ``Automerge/Document/marksAt(obj:position:)``

### Updating Text values

Expand Down
25 changes: 24 additions & 1 deletion Sources/Automerge/Cursor.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Foundation
import enum AutomergeUniffi.Position

typealias FfiPosition = AutomergeUniffi.Position

/// A opaque type that represents a location within an array or text object that adjusts with insertions and deletes to
/// maintain its relative position.
Expand All @@ -17,3 +19,24 @@ extension Cursor: CustomStringConvertible {
bytes.map { Swift.String(format: "%02hhx", $0) }.joined().uppercased()
}
}

/// An umbrella type that represents a location within an array or text object.
///
/// ### See Also
/// - ``Document/cursor(obj:position:)``
/// - ``Document/cursorAt(obj:position:heads:)``
public enum Position {
case cursor(Cursor)
case index(UInt64)
}

extension Position {
func toFfi() -> FfiPosition {
switch self {
case .cursor(let cursor):
return .cursor(position: cursor.bytes)
case .index(let index):
return .index(position: index)
}
}
}
89 changes: 89 additions & 0 deletions Sources/Automerge/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,95 @@ public final class Document: @unchecked Sendable {
}
}

/// Retrieves the list of marks within a text object at the specified position and point in time.
///
/// This method allows you to get the marks present at a specific position in a text object.
/// Marks can represent various formatting or annotations applied to the text.
///
/// - 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.
/// - 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.
///
/// # Example Usage
/// ```
/// let doc = Document()
/// let textId = try doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
///
/// let cursor = try doc.cursor(obj: textId, position: 0)
/// let marks = try doc.marksAt(obj: textId, position: .cursor(cursor), heads: doc.heads())
/// ```
///
/// ## 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.
///
/// ## 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.
///
/// - **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:)``
/// ``marksAt(obj:heads:)``
///
public func marksAt(obj: ObjId, position: Position, heads: Set<ChangeHash>) throws -> [Mark] {
try sync {
try self.doc.wrapErrors {
try $0.marksAtPosition(
obj: obj.bytes,
position: position.toFfi(),
heads: heads.map(\.bytes)
).map(Mark.fromFfi)
}
}
}

/// Retrieves the list of marks within a text object at the specified position.
///
/// This method allows you to get the marks present at a specific position in a text object.
/// Marks can represent various formatting or annotations applied to the text.
///
/// - 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.
/// - 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:)``.
///
/// # Example Usage
/// ```
/// let doc = Document()
/// let textId = try doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
///
/// let cursor = try doc.cursor(obj: textId, position: 0)
/// let marks = try doc.marksAt(obj: textId, position: .cursor(cursor), heads: doc.heads())
/// ```
///
/// ## 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.
///
/// ## 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.
///
/// - **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:)``
/// ``marksAt(obj:heads:)``
///
public func marksAt(obj: ObjId, position: Position) throws -> [Mark] {
try marksAt(obj: obj, position: position, heads: heads())
}

/// Commit the auto-generated transaction with options.
///
/// - Parameters:
Expand Down
31 changes: 31 additions & 0 deletions Tests/AutomergeTests/TestMarks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,35 @@ class MarksTestCase: XCTestCase {
path: [PathElement(obj: ObjId.ROOT, prop: .Key("text"))]
)])
}

func testMarksAtIndex() throws {
let doc = Document()
let textId = try doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello World!")
try doc.mark(obj: textId, start: 2, end: 5, expand: .both, name: "italic", value: .Boolean(true))
try doc.mark(obj: textId, start: 1, end: 5, expand: .both, name: "bold", value: .Boolean(true))

let marks = try doc.marksAt(obj: textId, position: .index(2))

XCTAssertEqual(marks, [
Mark(start: 2, end: 2, name: "bold", value: .Boolean(true)),
Mark(start: 2, end: 2, name: "italic", value: .Boolean(true))
])
}

func testMarksAtCursor() throws {
let doc = Document()
let textId = try doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello World!")
try doc.mark(obj: textId, start: 2, end: 5, expand: .both, name: "italic", value: .Boolean(true))
try doc.mark(obj: textId, start: 1, end: 5, expand: .both, name: "bold", value: .Boolean(true))

let cursor = try doc.cursor(obj: textId, position: 2)
let marks = try doc.marksAt(obj: textId, position: .cursor(cursor))

XCTAssertEqual(marks, [
Mark(start: 2, end: 2, name: "bold", value: .Boolean(true)),
Mark(start: 2, end: 2, name: "italic", value: .Boolean(true))
])
}
}
8 changes: 8 additions & 0 deletions rust/src/automerge.udl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ typedef sequence<u8> ActorId;
[Custom]
typedef sequence<u8> Cursor;

[Enum]
interface Position {
Cursor ( Cursor position );
Index ( u64 position );
};

[Enum]
interface ScalarValue {
Bytes( sequence<u8> value);
Expand Down Expand Up @@ -170,6 +176,8 @@ interface Doc {
sequence<Mark> marks(ObjId obj);
[Throws=DocError]
sequence<Mark> marks_at(ObjId obj, sequence<ChangeHash> heads);
[Throws=DocError]
sequence<Mark> marks_at_position(ObjId obj, Position position, sequence<ChangeHash> heads);

[Throws=DocError]
ObjId split_block(ObjId obj, u32 index);
Expand Down
5 changes: 5 additions & 0 deletions rust/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ use automerge as am;

pub struct Cursor(Vec<u8>);

pub enum Position {
Cursor { position: Cursor },
Index { position: u64 },
}

impl From<Cursor> for am::Cursor {
fn from(value: Cursor) -> Self {
am::Cursor::try_from(value.0).unwrap()
Expand Down
31 changes: 25 additions & 6 deletions rust/src/doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use automerge as am;
use automerge::{transaction::Transactable, ReadDoc};

use crate::actor_id::ActorId;
use crate::mark::{ExpandMark, Mark};
use crate::cursor::Position;
use crate::mark::{ExpandMark, KeyValue, Mark};
use crate::patches::Patch;
use crate::{
Change, ChangeHash, Cursor, ObjId, ObjType, PathElement, ScalarValue, SyncState, Value,
Expand Down Expand Up @@ -33,11 +34,6 @@ pub enum ReceiveSyncError {
InvalidMessage,
}

pub struct KeyValue {
pub key: String,
pub value: Value,
}

pub struct Doc(RwLock<automerge::AutoCommit>);

// These are okay because on the swift side we wrap all accesses of the
Expand Down Expand Up @@ -481,6 +477,29 @@ impl Doc {
.collect())
}

pub fn marks_at_position(
&self,
obj: ObjId,
position: Position,
heads: Vec<ChangeHash>,
) -> Result<Vec<Mark>, DocError> {
let obj = am::ObjId::from(obj);
let doc = self.0.read().unwrap();
assert_text(&*doc, &obj)?;
let heads = heads
.into_iter()
.map(am::ChangeHash::from)
.collect::<Vec<_>>();
let index = match position {
Position::Cursor { position: cursor } => doc
.get_cursor_position(obj.clone(), &cursor.into(), Some(&heads))
.unwrap() as usize,
Position::Index { position: index } => index as usize,
};
let markset = doc.get_marks(obj, index, Some(&heads)).unwrap();
Ok(Mark::from_markset(markset, index as u64))
}

pub fn split_block(&self, obj: ObjId, index: u32) -> Result<ObjId, DocError> {
let mut doc = self.0.write().unwrap();
let obj = am::ObjId::from(obj);
Expand Down
6 changes: 3 additions & 3 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ uniffi::include_scaffolding!("automerge");
mod actor_id;
use actor_id::ActorId;
mod cursor;
use cursor::Cursor;
use cursor::{Cursor, Position};
mod change;
use change::Change;
mod change_hash;
use change_hash::ChangeHash;
mod doc;
use doc::{Doc, DocError, KeyValue, LoadError, ReceiveSyncError};
use doc::{Doc, DocError, LoadError, ReceiveSyncError};
mod mark;
use mark::{ExpandMark, Mark};
use mark::{ExpandMark, KeyValue, Mark};
mod obj_id;
use obj_id::{root, ObjId};
mod obj_type;
Expand Down
23 changes: 22 additions & 1 deletion rust/src/mark.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use automerge as am;

use crate::ScalarValue;
use crate::{ScalarValue, Value};

pub enum ExpandMark {
Before,
Expand Down Expand Up @@ -37,3 +37,24 @@ impl<'a> From<&'a am::marks::Mark<'a>> for Mark {
}
}
}

pub struct KeyValue {
pub key: String,
pub value: Value,
}

impl Mark {
pub fn from_markset(mark_set: am::marks::MarkSet, index: u64) -> Vec<Mark> {
let mut result = Vec::new();
for (key, value) in mark_set.iter() {
let mark = Mark {
start: index,
end: index,
name: key.to_string(),
value: value.into(),
};
result.push(mark);
}
result
}
}
1 change: 1 addition & 0 deletions rust/src/obj_id.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::UniffiCustomTypeConverter;
use automerge as am;

#[derive(Debug, Clone)]
pub struct ObjId(Vec<u8>);

impl From<ObjId> for automerge::ObjId {
Expand Down

0 comments on commit 76e59a3

Please sign in to comment.