Files
core/README.yrs
Michael Stahl 88055a2f43 LOCRDT editeng: adapt to yrs v0.23.4 API change
Change-Id: I957cdaa770f34c48ca1283f6d0f3b89d66064f8c
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/186902
Reviewed-by: Michael Stahl <michael.stahl@allotropia.de>
Tested-by: Jenkins
2025-06-24 19:06:42 +02:00

131 lines
5.0 KiB
Plaintext

## Experimental Writer comments editing collaboration with yrs
### How to build
First, build yrs C FFI bindings:
```
git clone https://github.com/y-crdt/y-crdt.git
cd y-crdt
git checkout v0.23.5
cargo build -p yffi
```
Then, put the yrs build directory in autogen.input:
`--with-yrs=/path/to/y-crdt`
All the related code should be behind macros in `config_host/config_collab.h`
### How to run
To prevent crashes at runtime, set the environment variable
EDIT_COMMENT_IN_READONLY_MODE=1 and open documents in read-only mode: only
inserting/deleting comments, and editing inside comments will be enabled.
Currently, communication happens over a hard-coded pipe:
* start an soffice with YRSACCEPT=1 load a Writer document and it will listen
and block until connect
(you can also create a new Writer document but that will be boring if all
you can do is insert comments into empty doc)
* start another soffice with YRSCONNECT=1 with a different user profile,
create new Writer document, and it will connect and load the document from
the other side
All sorts of paragraph and character formattings should work inside comments.
Peer cursors should be displayed both in the sw document body and inside
comments.
Inserting hyperlinks also works, although sadly i wasn't able to figure out
how to enable the menu items in read-only mode, so it only works in editable
mode.
Switching to editable mode is also possible, but only comment-related editing
is synced via yrs, so if other editing operations change the positions of
comments, a crash will be inevitable.
### Implementation
Most of it is in 2 classes: EditDoc and sw::DocumentStateManager (for now);
the latter gets a new member YrsTransactionSupplier.
DocumentStateManager starts a thread to communicate, and this sends new
messages to the main thread via PostUserEvent().
The EditDoc models of the comments are duplicated in a yrs YDocument model
and this is then synced remotely by yrs.
The structure of the yrs model is:
* YMap of comments (key: CommentId created from peer id + counter)
- YArray
- anchor pos: 2 or 4 ints [manually updated when editing sw]
- YMap of comment properties
- YText containing mapped EditDoc state
* YMap of cursors (key: peer id)
- either sw cursor: 2 or 4 ints [manually updated when editing sw]
or EditDoc position: CommentId + 2 or 4 ints, or WeakRef
or Y_JSON_NULL (for sw non-text selections, effectively ignored)
Some confusing object relationships:
SwAnnotationWin -> Outliner -> OutlinerEditEng -> EditEngine -> ImpEditEngine -> EditDoc
-> OutlinerView -> EditView -> ImpEditView -> EditEngine
-> SidebarTextControl
### Undo
There was no Undo for edits inside comments anyway, only when losing the
focus a SwUndoFieldFromDoc is created.
There are 2 approaches how Undo could work: either let all the SwUndo
actions modify the yrs model, or use the yrs yundo_manager and ensure that
for every top-level SwUndo there is exactly one item in the yundo_manager's
stack, so that Undo/Redo will have the same effect in the sw model and
the yrs model.
Let's try if the second approach can be made to work.
The yundo_manager by default creates stack items based on a timer, so we
configure that to a 2^31 timeout and invoke yundo_manager_stop() to create
all the items manually.
yundo_manager_undo()/redo() etc internally create a YTransaction and commit
it, which will of course fail if one already exists!
The yundo_manager creates a new item also for things like cursor movements
(because we put the cursors in the same YDocument as the content); there is
a way to filter the changes by Branch, but that filtering happens when the
undo/redo is invoked, not when the stack item is created - so the
"temporary" stack item is always extended with yrs changes until a "real"
change that has a corresponding SwUndo happens and there is a corresponding
yundo_manager_stop() then.
There are still some corner cases where the 2 undo stacks aren't synced so
there are various workarounds like DummyUndo action or m_nTempUndoOffset
counter for these.
Also the SwUndoFieldFromDoc batch action is problematic: it is created when
the comment loses focus, but all editing operations in the comment are
inserted in the yrs model immediately as they happen - so if changes are
received from peers, the creation of the SwUndoFieldFromDoc must be forced
to happen before the peer changes are applied in the sw document model, to
keep the actions in order.
The comment id is used as a key in yrs ymap so it should be the same when
the Undo or Redo inserts a comment again, hence it's added to SwPostItField.
For edits that are received from peers, what happens is that all the
received changes must be grouped into one SwUndo (list) action because there
is going to be one yrs undo item; and invoking Undo will just send the
changes to peers and they will create a new undo stack item as the local
instance goes back in the undo stack, and it's not possible to do it
differently because the undo stack items may contain different changes on
each peer.