Skip to content

Commit 76e59a3

Browse files
exposing Automerge::get_marks through to Swift (#186)
* 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 <[email protected]> * 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 <[email protected]>
1 parent d12280d commit 76e59a3

File tree

11 files changed

+211
-11
lines changed

11 files changed

+211
-11
lines changed

Sources/Automerge/Automerge.docc/Curation/Document.md

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
- ``text(obj:)``
5252
- ``length(obj:)``
5353
- ``marks(obj:)``
54+
- ``marksAt(obj:position:)``
5455

5556
### Updating Text values
5657

@@ -100,6 +101,7 @@
100101
- ``textAt(obj:heads:)``
101102
- ``lengthAt(obj:heads:)``
102103
- ``marksAt(obj:heads:)``
104+
- ``marksAt(obj:position:heads:)``
103105

104106
### Saving, forking, and merging documents
105107

Sources/Automerge/Automerge.docc/ModelingData.md

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ See the documentation for ``Document`` for more detail on the individual methods
109109
- ``Automerge/Document/text(obj:)``
110110
- ``Automerge/Document/length(obj:)``
111111
- ``Automerge/Document/marks(obj:)``
112+
- ``Automerge/Document/marksAt(obj:position:)``
112113

113114
### Updating Text values
114115

Sources/Automerge/Cursor.swift

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import Foundation
1+
import enum AutomergeUniffi.Position
2+
3+
typealias FfiPosition = AutomergeUniffi.Position
24

35
/// A opaque type that represents a location within an array or text object that adjusts with insertions and deletes to
46
/// maintain its relative position.
@@ -17,3 +19,24 @@ extension Cursor: CustomStringConvertible {
1719
bytes.map { Swift.String(format: "%02hhx", $0) }.joined().uppercased()
1820
}
1921
}
22+
23+
/// An umbrella type that represents a location within an array or text object.
24+
///
25+
/// ### See Also
26+
/// - ``Document/cursor(obj:position:)``
27+
/// - ``Document/cursorAt(obj:position:heads:)``
28+
public enum Position {
29+
case cursor(Cursor)
30+
case index(UInt64)
31+
}
32+
33+
extension Position {
34+
func toFfi() -> FfiPosition {
35+
switch self {
36+
case .cursor(let cursor):
37+
return .cursor(position: cursor.bytes)
38+
case .index(let index):
39+
return .index(position: index)
40+
}
41+
}
42+
}

Sources/Automerge/Document.swift

+89
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,95 @@ public final class Document: @unchecked Sendable {
768768
}
769769
}
770770

771+
/// Retrieves the list of marks within a text object at the specified position and point in time.
772+
///
773+
/// This method allows you to get the marks present at a specific position in a text object.
774+
/// Marks can represent various formatting or annotations applied to the text.
775+
///
776+
/// - Parameters:
777+
/// - obj: The identifier of the text object, represented by an ``ObjId``.
778+
/// - position: The position within the text, represented by a ``Position`` enum which can be a ``Cursor`` or an `UInt64` as a fixed position.
779+
/// - heads: A set of `ChangeHash` values that represents a point in time in the document's history.
780+
/// - Returns: An array of `Mark` objects for the text object at the specified position.
781+
///
782+
/// # Example Usage
783+
/// ```
784+
/// let doc = Document()
785+
/// let textId = try doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
786+
///
787+
/// let cursor = try doc.cursor(obj: textId, position: 0)
788+
/// let marks = try doc.marksAt(obj: textId, position: .cursor(cursor), heads: doc.heads())
789+
/// ```
790+
///
791+
/// ## Recommendation
792+
/// Use this method to query the marks applied to a text object at a specific position.
793+
/// This can be useful for retrieving ``Marks`` related to a character without traversing the full document.
794+
///
795+
/// ## When to Use Cursor vs. Index
796+
///
797+
/// While you can specify the position either with a `Cursor` or an `Index`, there are important distinctions:
798+
///
799+
/// - **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.
800+
///
801+
/// - **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.
802+
///
803+
/// # See Also
804+
/// ``marksAt(obj:position:)``
805+
/// ``marksAt(obj:heads:)``
806+
///
807+
public func marksAt(obj: ObjId, position: Position, heads: Set<ChangeHash>) throws -> [Mark] {
808+
try sync {
809+
try self.doc.wrapErrors {
810+
try $0.marksAtPosition(
811+
obj: obj.bytes,
812+
position: position.toFfi(),
813+
heads: heads.map(\.bytes)
814+
).map(Mark.fromFfi)
815+
}
816+
}
817+
}
818+
819+
/// Retrieves the list of marks within a text object at the specified position.
820+
///
821+
/// This method allows you to get the marks present at a specific position in a text object.
822+
/// Marks can represent various formatting or annotations applied to the text.
823+
///
824+
/// - Parameters:
825+
/// - obj: The identifier of the text object, represented by an ``ObjId``.
826+
/// - position: The position within the text, represented by a ``Position`` enum which can be a ``Cursor`` or an `UInt64` as a fixed position.
827+
/// - Returns: An array of `Mark` objects for the text object at the specified position.
828+
/// - Note: This method retrieves marks from the latest version of the document.
829+
/// If you need to specify a point in the document's history, refer to ``marksAt(obj:position:heads:)``.
830+
///
831+
/// # Example Usage
832+
/// ```
833+
/// let doc = Document()
834+
/// let textId = try doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
835+
///
836+
/// let cursor = try doc.cursor(obj: textId, position: 0)
837+
/// let marks = try doc.marksAt(obj: textId, position: .cursor(cursor), heads: doc.heads())
838+
/// ```
839+
///
840+
/// ## Recommendation
841+
/// Use this method to query the marks applied to a text object at a specific position.
842+
/// This can be useful for retrieving ``Marks`` related to a character without traversing the full document.
843+
///
844+
/// ## When to Use Cursor vs. Index
845+
///
846+
/// While you can specify the position either with a `Cursor` or an `Index`, there are important distinctions:
847+
///
848+
/// - **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.
849+
///
850+
/// - **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.
851+
///
852+
/// # See Also
853+
/// ``marksAt(obj:position:heads:)``
854+
/// ``marksAt(obj:heads:)``
855+
///
856+
public func marksAt(obj: ObjId, position: Position) throws -> [Mark] {
857+
try marksAt(obj: obj, position: position, heads: heads())
858+
}
859+
771860
/// Commit the auto-generated transaction with options.
772861
///
773862
/// - Parameters:

Tests/AutomergeTests/TestMarks.swift

+31
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,35 @@ class MarksTestCase: XCTestCase {
6565
path: [PathElement(obj: ObjId.ROOT, prop: .Key("text"))]
6666
)])
6767
}
68+
69+
func testMarksAtIndex() throws {
70+
let doc = Document()
71+
let textId = try doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
72+
try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello World!")
73+
try doc.mark(obj: textId, start: 2, end: 5, expand: .both, name: "italic", value: .Boolean(true))
74+
try doc.mark(obj: textId, start: 1, end: 5, expand: .both, name: "bold", value: .Boolean(true))
75+
76+
let marks = try doc.marksAt(obj: textId, position: .index(2))
77+
78+
XCTAssertEqual(marks, [
79+
Mark(start: 2, end: 2, name: "bold", value: .Boolean(true)),
80+
Mark(start: 2, end: 2, name: "italic", value: .Boolean(true))
81+
])
82+
}
83+
84+
func testMarksAtCursor() throws {
85+
let doc = Document()
86+
let textId = try doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
87+
try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello World!")
88+
try doc.mark(obj: textId, start: 2, end: 5, expand: .both, name: "italic", value: .Boolean(true))
89+
try doc.mark(obj: textId, start: 1, end: 5, expand: .both, name: "bold", value: .Boolean(true))
90+
91+
let cursor = try doc.cursor(obj: textId, position: 2)
92+
let marks = try doc.marksAt(obj: textId, position: .cursor(cursor))
93+
94+
XCTAssertEqual(marks, [
95+
Mark(start: 2, end: 2, name: "bold", value: .Boolean(true)),
96+
Mark(start: 2, end: 2, name: "italic", value: .Boolean(true))
97+
])
98+
}
6899
}

rust/src/automerge.udl

+8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ typedef sequence<u8> ActorId;
1414
[Custom]
1515
typedef sequence<u8> Cursor;
1616

17+
[Enum]
18+
interface Position {
19+
Cursor ( Cursor position );
20+
Index ( u64 position );
21+
};
22+
1723
[Enum]
1824
interface ScalarValue {
1925
Bytes( sequence<u8> value);
@@ -170,6 +176,8 @@ interface Doc {
170176
sequence<Mark> marks(ObjId obj);
171177
[Throws=DocError]
172178
sequence<Mark> marks_at(ObjId obj, sequence<ChangeHash> heads);
179+
[Throws=DocError]
180+
sequence<Mark> marks_at_position(ObjId obj, Position position, sequence<ChangeHash> heads);
173181

174182
[Throws=DocError]
175183
ObjId split_block(ObjId obj, u32 index);

rust/src/cursor.rs

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ use automerge as am;
33

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

6+
pub enum Position {
7+
Cursor { position: Cursor },
8+
Index { position: u64 },
9+
}
10+
611
impl From<Cursor> for am::Cursor {
712
fn from(value: Cursor) -> Self {
813
am::Cursor::try_from(value.0).unwrap()

rust/src/doc.rs

+25-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use automerge as am;
55
use automerge::{transaction::Transactable, ReadDoc};
66

77
use crate::actor_id::ActorId;
8-
use crate::mark::{ExpandMark, Mark};
8+
use crate::cursor::Position;
9+
use crate::mark::{ExpandMark, KeyValue, Mark};
910
use crate::patches::Patch;
1011
use crate::{
1112
Change, ChangeHash, Cursor, ObjId, ObjType, PathElement, ScalarValue, SyncState, Value,
@@ -33,11 +34,6 @@ pub enum ReceiveSyncError {
3334
InvalidMessage,
3435
}
3536

36-
pub struct KeyValue {
37-
pub key: String,
38-
pub value: Value,
39-
}
40-
4137
pub struct Doc(RwLock<automerge::AutoCommit>);
4238

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

480+
pub fn marks_at_position(
481+
&self,
482+
obj: ObjId,
483+
position: Position,
484+
heads: Vec<ChangeHash>,
485+
) -> Result<Vec<Mark>, DocError> {
486+
let obj = am::ObjId::from(obj);
487+
let doc = self.0.read().unwrap();
488+
assert_text(&*doc, &obj)?;
489+
let heads = heads
490+
.into_iter()
491+
.map(am::ChangeHash::from)
492+
.collect::<Vec<_>>();
493+
let index = match position {
494+
Position::Cursor { position: cursor } => doc
495+
.get_cursor_position(obj.clone(), &cursor.into(), Some(&heads))
496+
.unwrap() as usize,
497+
Position::Index { position: index } => index as usize,
498+
};
499+
let markset = doc.get_marks(obj, index, Some(&heads)).unwrap();
500+
Ok(Mark::from_markset(markset, index as u64))
501+
}
502+
484503
pub fn split_block(&self, obj: ObjId, index: u32) -> Result<ObjId, DocError> {
485504
let mut doc = self.0.write().unwrap();
486505
let obj = am::ObjId::from(obj);

rust/src/lib.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ uniffi::include_scaffolding!("automerge");
33
mod actor_id;
44
use actor_id::ActorId;
55
mod cursor;
6-
use cursor::Cursor;
6+
use cursor::{Cursor, Position};
77
mod change;
88
use change::Change;
99
mod change_hash;
1010
use change_hash::ChangeHash;
1111
mod doc;
12-
use doc::{Doc, DocError, KeyValue, LoadError, ReceiveSyncError};
12+
use doc::{Doc, DocError, LoadError, ReceiveSyncError};
1313
mod mark;
14-
use mark::{ExpandMark, Mark};
14+
use mark::{ExpandMark, KeyValue, Mark};
1515
mod obj_id;
1616
use obj_id::{root, ObjId};
1717
mod obj_type;

rust/src/mark.rs

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use automerge as am;
22

3-
use crate::ScalarValue;
3+
use crate::{ScalarValue, Value};
44

55
pub enum ExpandMark {
66
Before,
@@ -37,3 +37,24 @@ impl<'a> From<&'a am::marks::Mark<'a>> for Mark {
3737
}
3838
}
3939
}
40+
41+
pub struct KeyValue {
42+
pub key: String,
43+
pub value: Value,
44+
}
45+
46+
impl Mark {
47+
pub fn from_markset(mark_set: am::marks::MarkSet, index: u64) -> Vec<Mark> {
48+
let mut result = Vec::new();
49+
for (key, value) in mark_set.iter() {
50+
let mark = Mark {
51+
start: index,
52+
end: index,
53+
name: key.to_string(),
54+
value: value.into(),
55+
};
56+
result.push(mark);
57+
}
58+
result
59+
}
60+
}

rust/src/obj_id.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::UniffiCustomTypeConverter;
22
use automerge as am;
33

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

67
impl From<ObjId> for automerge::ObjId {

0 commit comments

Comments
 (0)