| Michael Stahl | 8f80341 | 2024-10-25 15:02:34 +0200 | [diff] [blame] | 1 | |
| 2 | ## Experimental Writer comments editing collaboration with yrs |
| 3 | |
| 4 | ### How to build |
| 5 | |
| 6 | First, build yrs C FFI bindings: |
| 7 | |
| 8 | ``` |
| 9 | git clone https://github.com/y-crdt/y-crdt.git |
| 10 | cd y-crdt |
| Michael Stahl | 88055a2 | 2025-06-24 15:32:52 +0200 | [diff] [blame] | 11 | git checkout v0.23.5 |
| Michael Stahl | 8f80341 | 2024-10-25 15:02:34 +0200 | [diff] [blame] | 12 | cargo build -p yffi |
| 13 | ``` |
| 14 | |
| 15 | Then, put the yrs build directory in autogen.input: |
| 16 | |
| 17 | `--with-yrs=/path/to/y-crdt` |
| 18 | |
| Michael Stahl | 094480a | 2025-05-26 11:36:04 +0200 | [diff] [blame] | 19 | All the related code should be behind macros in `config_host/config_collab.h` |
| 20 | |
| Michael Stahl | 8f80341 | 2024-10-25 15:02:34 +0200 | [diff] [blame] | 21 | ### How to run |
| 22 | |
| 23 | To prevent crashes at runtime, set the environment variable |
| 24 | EDIT_COMMENT_IN_READONLY_MODE=1 and open documents in read-only mode: only |
| 25 | inserting/deleting comments, and editing inside comments will be enabled. |
| 26 | |
| 27 | Currently, communication happens over a hard-coded pipe: |
| 28 | |
| 29 | * start an soffice with YRSACCEPT=1 load a Writer document and it will listen |
| 30 | and block until connect |
| 31 | (you can also create a new Writer document but that will be boring if all |
| 32 | you can do is insert comments into empty doc) |
| 33 | |
| Michael Stahl | 094480a | 2025-05-26 11:36:04 +0200 | [diff] [blame] | 34 | * start another soffice with YRSCONNECT=1 with a different user profile, |
| 35 | create new Writer document, and it will connect and load the document from |
| 36 | the other side |
| Michael Stahl | 8f80341 | 2024-10-25 15:02:34 +0200 | [diff] [blame] | 37 | |
| 38 | All sorts of paragraph and character formattings should work inside comments. |
| 39 | |
| Michael Stahl | 094480a | 2025-05-26 11:36:04 +0200 | [diff] [blame] | 40 | Peer cursors should be displayed both in the sw document body and inside |
| 41 | comments. |
| 42 | |
| Michael Stahl | 8f80341 | 2024-10-25 15:02:34 +0200 | [diff] [blame] | 43 | Inserting hyperlinks also works, although sadly i wasn't able to figure out |
| 44 | how to enable the menu items in read-only mode, so it only works in editable |
| 45 | mode. |
| 46 | |
| Michael Stahl | 8f80341 | 2024-10-25 15:02:34 +0200 | [diff] [blame] | 47 | Switching to editable mode is also possible, but only comment-related editing |
| 48 | is synced via yrs, so if other editing operations change the positions of |
| 49 | comments, a crash will be inevitable. |
| 50 | |
| Michael Stahl | 094480a | 2025-05-26 11:36:04 +0200 | [diff] [blame] | 51 | ### Implementation |
| 52 | |
| 53 | Most of it is in 2 classes: EditDoc and sw::DocumentStateManager (for now); |
| 54 | the latter gets a new member YrsTransactionSupplier. |
| 55 | |
| 56 | DocumentStateManager starts a thread to communicate, and this sends new |
| 57 | messages to the main thread via PostUserEvent(). |
| 58 | |
| 59 | The EditDoc models of the comments are duplicated in a yrs YDocument model |
| 60 | and this is then synced remotely by yrs. |
| 61 | |
| 62 | The structure of the yrs model is: |
| 63 | |
| 64 | * YMap of comments (key: CommentId created from peer id + counter) |
| 65 | - YArray |
| 66 | - anchor pos: 2 or 4 ints [manually updated when editing sw] |
| 67 | - YMap of comment properties |
| 68 | - YText containing mapped EditDoc state |
| 69 | * YMap of cursors (key: peer id) |
| 70 | - either sw cursor: 2 or 4 ints [manually updated when editing sw] |
| 71 | or EditDoc position: CommentId + 2 or 4 ints, or WeakRef |
| 72 | or Y_JSON_NULL (for sw non-text selections, effectively ignored) |
| 73 | |
| 74 | Some confusing object relationships: |
| 75 | |
| 76 | SwAnnotationWin -> Outliner -> OutlinerEditEng -> EditEngine -> ImpEditEngine -> EditDoc |
| 77 | -> OutlinerView -> EditView -> ImpEditView -> EditEngine |
| 78 | |
| 79 | -> SidebarTextControl |
| 80 | |
| 81 | ### Undo |
| 82 | |
| 83 | There was no Undo for edits inside comments anyway, only when losing the |
| 84 | focus a SwUndoFieldFromDoc is created. |
| 85 | |
| 86 | There are 2 approaches how Undo could work: either let all the SwUndo |
| 87 | actions modify the yrs model, or use the yrs yundo_manager and ensure that |
| 88 | for every top-level SwUndo there is exactly one item in the yundo_manager's |
| 89 | stack, so that Undo/Redo will have the same effect in the sw model and |
| 90 | the yrs model. |
| 91 | |
| 92 | Let's try if the second approach can be made to work. |
| 93 | |
| 94 | The yundo_manager by default creates stack items based on a timer, so we |
| 95 | configure that to a 2^31 timeout and invoke yundo_manager_stop() to create |
| 96 | all the items manually. |
| 97 | |
| 98 | yundo_manager_undo()/redo() etc internally create a YTransaction and commit |
| 99 | it, which will of course fail if one already exists! |
| 100 | |
| 101 | The yundo_manager creates a new item also for things like cursor movements |
| 102 | (because we put the cursors in the same YDocument as the content); there is |
| 103 | a way to filter the changes by Branch, but that filtering happens when the |
| 104 | undo/redo is invoked, not when the stack item is created - so the |
| 105 | "temporary" stack item is always extended with yrs changes until a "real" |
| 106 | change that has a corresponding SwUndo happens and there is a corresponding |
| 107 | yundo_manager_stop() then. |
| 108 | |
| 109 | There are still some corner cases where the 2 undo stacks aren't synced so |
| 110 | there are various workarounds like DummyUndo action or m_nTempUndoOffset |
| 111 | counter for these. |
| 112 | |
| 113 | Also the SwUndoFieldFromDoc batch action is problematic: it is created when |
| 114 | the comment loses focus, but all editing operations in the comment are |
| 115 | inserted in the yrs model immediately as they happen - so if changes are |
| 116 | received from peers, the creation of the SwUndoFieldFromDoc must be forced |
| 117 | to happen before the peer changes are applied in the sw document model, to |
| 118 | keep the actions in order. |
| 119 | |
| 120 | The comment id is used as a key in yrs ymap so it should be the same when |
| 121 | the Undo or Redo inserts a comment again, hence it's added to SwPostItField. |
| 122 | |
| 123 | For edits that are received from peers, what happens is that all the |
| 124 | received changes must be grouped into one SwUndo (list) action because there |
| 125 | is going to be one yrs undo item; and invoking Undo will just send the |
| 126 | changes to peers and they will create a new undo stack item as the local |
| 127 | instance goes back in the undo stack, and it's not possible to do it |
| 128 | differently because the undo stack items may contain different changes on |
| 129 | each peer. |
| 130 | |