Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exposing Automerge::get_marks through to Swift #186

Merged
merged 13 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
7 changes: 6 additions & 1 deletion rust/src/cursor.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
use super::UniffiCustomTypeConverter;
use automerge as am;

pub struct Cursor(Vec<u8>);
pub struct Cursor(pub Vec<u8>);

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

impl From<Cursor> for am::Cursor {
fn from(value: Cursor) -> Self {
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};
heckj marked this conversation as resolved.
Show resolved Hide resolved

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,
heckj marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading