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

Update EditReverse operation to support undo and redo on Text.edit #649

Draft
wants to merge 63 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
887d4cf
add TODO requirement comments
hyemmie Aug 24, 2023
15393ab
add test code for initial undo/redo
hyemmie Aug 25, 2023
738ce5f
add history at document
hyemmie Aug 25, 2023
6e69505
feat reverse operation for counter increase
hyemmie Aug 25, 2023
98bc22e
feat undo function
hyemmie Aug 25, 2023
baf514a
temp for test
hyemmie Aug 25, 2023
3063ac6
Fix update() function
hyemmie Aug 28, 2023
f1c47f0
Add TODO comment for undefined executedAt
hyemmie Aug 28, 2023
554f0d2
Feat redo function
hyemmie Aug 28, 2023
9fe1c14
Add redo and no change test cases
hyemmie Aug 28, 2023
69cc857
Add undo/redo error handling and test code
hyemmie Aug 28, 2023
2fbae1e
Add assertUndoRedo and handle undo/redo for Long type
chacha912 Aug 28, 2023
3c81cc4
Add max stack size for undo/redo and test code
hyemmie Aug 28, 2023
a64ed15
Merge branch 'feat/undo-redo-arch' of https://github.com/yorkie-team/…
hyemmie Aug 28, 2023
dc1fd74
Fix long counter multiply function type error
hyemmie Aug 28, 2023
6c36a3c
Add test for reverse operation of increase operation
chacha912 Aug 28, 2023
6790ee1
Add test for changeInfo when undoing increase operation
chacha912 Aug 28, 2023
fe2400c
Add redo stack clear function and test code
hyemmie Aug 28, 2023
eeea054
Merge branch 'feat/undo-redo-arch' of https://github.com/yorkie-team/…
chacha912 Aug 28, 2023
cee8126
Implement undo/redo with presence
chacha912 Aug 31, 2023
48f7769
Merge branch 'main' of https://github.com/yorkie-team/yorkie-js-sdk i…
chacha912 Sep 1, 2023
ccb371a
Feat reverse operation of object set, remove
hyemmie Sep 1, 2023
5344222
Update test code for object undo/redo
hyemmie Sep 1, 2023
6261f54
Add concurrent object undo/redo tests
hyemmie Sep 4, 2023
99d48a7
Add codemirror devtool
chacha912 Sep 3, 2023
95b4217
Fix set reverse operation bug
hyemmie Sep 5, 2023
7b0d604
Add assertUndoRedo to object_test
hyemmie Sep 5, 2023
c62a0b9
Update document_test
hyemmie Sep 5, 2023
3d18f1c
Merge branch 'feat/undo-redo-arch' of https://github.com/yorkie-team/…
chacha912 Sep 5, 2023
550ce85
Add reverse operation of text.edit
chacha912 Sep 5, 2023
3b2e3b1
Add undo/redo in codemirror example
chacha912 Sep 5, 2023
38238e4
Add test cases that fail to share for debugging
chacha912 Sep 5, 2023
2a9c0bb
Cleanup test code
chacha912 Sep 6, 2023
e6aa29e
Implement the reverse operation of text.edit
hyemmie Sep 8, 2023
76370be
Update protocol buffer
hyemmie Sep 11, 2023
32304d0
Add concurrent test cases of text.edit undo
hyemmie Sep 11, 2023
b7ca4ea
Implement EditReverse Op based on node ID
hyemmie Sep 12, 2023
6e07a76
Fix empty insertedID bug
hyemmie Sep 13, 2023
b476a45
Add assertUndoRedo at text_test
hyemmie Sep 13, 2023
262e8ac
Add todo comment about GC
hyemmie Sep 13, 2023
b92d3b6
Remove useless console and test condition
hyemmie Sep 13, 2023
7bf26e2
Remove comment on garbagecollect function
hyemmie Sep 13, 2023
7e4ecf0
Cleanup undo/redo test for simple object and nested object
chacha912 Sep 13, 2023
7ddba3c
Modify to execute reverse operations in reverse order within change
chacha912 Sep 13, 2023
4b1d850
Update protocol buffer to add EditReverse
hyemmie Sep 13, 2023
b3edc4c
Add text.edit concurrent test cases
hyemmie Sep 13, 2023
73df8a7
Implement EditReverse's change creation
hyemmie Sep 15, 2023
87f233b
Update test codes
hyemmie Sep 15, 2023
1cd46fb
Merge branch 'feat/undo-redo-arch' into feat/text-edit-reverse
hyemmie Sep 15, 2023
29a2735
Optimize findAllSplitNodesWithinLength
hyemmie Sep 15, 2023
1fce724
Fix merge conflict
hyemmie Sep 18, 2023
7567895
Add DisableGC option to document (#644)
hackerwins Sep 15, 2023
2d65b2e
Add gc test code
hyemmie Sep 18, 2023
1639835
Modify to enable creating CRDTElement with same createdAt
chacha912 Sep 14, 2023
3dfb748
Merge branch 'feat/undo-redo-arch' of https://github.com/yorkie-team/…
chacha912 Sep 19, 2023
fd6ae03
Merge branch 'main' of https://github.com/yorkie-team/yorkie-js-sdk i…
chacha912 Sep 19, 2023
a9c5d2f
Add undo and redo shortcuts to CodeMirror example
chacha912 Sep 19, 2023
d2a88c2
Merge branch 'feat/undo-redo-arch' of https://github.com/yorkie-team/…
chacha912 Sep 19, 2023
66b8d4e
Update selection during local undo in codemirror example
chacha912 Sep 19, 2023
7ce4303
Merge remote-tracking branch 'origin/main' into feat/text-edit-reverse
chacha912 Sep 25, 2023
9fc9ab8
Remove code related to object set and remove
chacha912 Sep 25, 2023
5f9263f
Replace TextNodeIDWithLength to RGATreeSplitPos
chacha912 Sep 25, 2023
5716e26
Remove createdAtMapByActor in EditReverseOperation
chacha912 Sep 25, 2023
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
6 changes: 6 additions & 0 deletions public/devtool/text.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@
height: 100%;
}

.layout > .content > .editor-area .editor-control {
position: absolute;
bottom: 0;
right: 0;
}

.layout > .content > .editor-area > .data-area {
display: flex;
}
Expand Down
67 changes: 59 additions & 8 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<div class="layout">
<div class="toolbar">
<div class="left-tools tools">
<!-- yorkie logo -->
<svg
xmlns="http://www.w3.org/2000/svg"
width="100"
Expand Down Expand Up @@ -116,6 +117,10 @@
<div class="editor-area">
<div class="codemirror-area">
<textarea id="textarea" cols="30" rows="10"></textarea>
<div class="editor-control">
<button class="undo-button">undo</button>
<button class="redo-button">redo</button>
</div>
</div>
<div class="data-area" id="view-text">
<div class="text-view-area">
Expand Down Expand Up @@ -332,7 +337,9 @@ <h4 class="title">
await client.activate();

// 03-1. create a document then attach it into the client.
const doc = new yorkie.Document('codemirror');
const doc = new yorkie.Document('codemirror', {
disableGC: true,
});
doc.subscribe('presence', (event) => {
if (event.type === 'presence-changed') return;
displayUsers(doc.getPresences(), client.getID());
Expand Down Expand Up @@ -367,9 +374,27 @@ <h4 class="title">
displayRemoteSelection(codemirror, doc, event.value);
}
});
doc.subscribe('my-presence', (event) => {
if (
event.type === 'presence-changed' &&
event.message === 'YORKIE_HISTORY'
) {
const [fromIdx, toIdx] = doc
.getRoot()
.content.posRangeToIndexRange(event.value.presence.selection);

const from = codemirror.posFromIndex(fromIdx);
const to = codemirror.posFromIndex(toIdx);
codemirror.setSelection(from, to, { origin: 'yorkie' });
}
});

doc.subscribe('$.content', (event) => {
if (event.type === 'remote-change') {
if (
event.type === 'remote-change' ||
(event.type === 'local-change' &&
event.value.message === 'YORKIE_HISTORY')
) {
const { actor, operations } = event.value;
handleOperations(operations, actor);

Expand Down Expand Up @@ -397,9 +422,12 @@ <h4 class="title">

doc.update((root, presence) => {
const range = root.content.edit(from, to, content);
presence.set({
selection: root.content.indexRangeToPosRange(range),
});
presence.set(
{
selection: root.content.indexRangeToPosRange(range),
},
{ addToHistory: true },
);
}, `update content by ${client.getID()}`);
console.log(`%c local: ${from}-${to}: ${content}`, 'color: green');
});
Expand All @@ -422,13 +450,12 @@ <h4 class="title">
// NOTE: The following conditional statement ignores cursor changes
// that occur while applying remote changes to CodeMirror
// and handles only movement by keyboard and mouse.
if (change.origin === undefined) {
console.log(change.origin);
if (change.origin === undefined || change.origin === 'yorkie') {
return;
}

const from = cm.indexFromPos(change.ranges[0].anchor);
const to = cm.indexFromPos(change.ranges[0].head);

doc.update((root, presence) => {
presence.set({
selection: root.content.indexRangeToPosRange([from, to]),
Expand All @@ -455,6 +482,30 @@ <h4 class="title">
}
}

// Undo and Redo
document
.querySelector('.undo-button')
.addEventListener('click', () => {
console.log('undo op', doc.getUndoStackForTest().at(-1));
doc.undo();
});
document
.querySelector('.redo-button')
.addEventListener('click', () => {
console.log('redo op', doc.getRedoStackForTest().at(-1));
doc.redo();
});
codemirror.addKeyMap({
'Cmd-Z': (cm) => {
console.log('undo op', doc.getUndoStackForTest().at(-1));
doc.history.undo();
},
'Shift-Cmd-Z': (cm) => {
console.log('redo op', doc.getRedoStackForTest().at(-1));
doc.history.redo();
},
});

// 05. synchronize text of document and codemirror.
codemirror.setValue(doc.getRoot().content.toString());
devtool.displayLog(doc, codemirror);
Expand Down
78 changes: 68 additions & 10 deletions src/api/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { AddOperation } from '@yorkie-js-sdk/src/document/operation/add_operatio
import { MoveOperation } from '@yorkie-js-sdk/src/document/operation/move_operation';
import { RemoveOperation } from '@yorkie-js-sdk/src/document/operation/remove_operation';
import { EditOperation } from '@yorkie-js-sdk/src/document/operation/edit_operation';
import { EditReverseOperation } from '@yorkie-js-sdk/src/document/operation/edit_reverse_operation';
import { StyleOperation } from '@yorkie-js-sdk/src/document/operation/style_operation';
import { TreeEditOperation } from '@yorkie-js-sdk/src/document/operation/tree_edit_operation';
import { ChangeID } from '@yorkie-js-sdk/src/document/change/change_id';
Expand Down Expand Up @@ -339,8 +340,10 @@ function toOperation(operation: Operation): PbOperation {
pbEditOperation.setFrom(toTextNodePos(editOperation.getFromPos()));
pbEditOperation.setTo(toTextNodePos(editOperation.getToPos()));
const pbCreatedAtMapByActor = pbEditOperation.getCreatedAtMapByActorMap();
for (const [key, value] of editOperation.getMaxCreatedAtMapByActor()) {
pbCreatedAtMapByActor.set(key, toTimeTicket(value)!);
if (editOperation.getMaxCreatedAtMapByActor()) {
for (const [key, value] of editOperation.getMaxCreatedAtMapByActor()!) {
pbCreatedAtMapByActor.set(key, toTimeTicket(value)!);
}
}
pbEditOperation.setContent(editOperation.getContent());
const pbAttributes = pbEditOperation.getAttributesMap();
Expand All @@ -349,6 +352,34 @@ function toOperation(operation: Operation): PbOperation {
}
pbEditOperation.setExecutedAt(toTimeTicket(editOperation.getExecutedAt()));
pbOperation.setEdit(pbEditOperation);
} else if (operation instanceof EditReverseOperation) {
const editReverseOperation = operation as EditReverseOperation;
const pbEditReverseOperation = new PbOperation.EditReverse();
pbEditReverseOperation.setParentCreatedAt(
toTimeTicket(editReverseOperation.getParentCreatedAt()),
);

const pbDeletedIDs = [];
const deletedIDs = editReverseOperation.getDeletedIDs();
for (const deletedID of deletedIDs) {
pbDeletedIDs.push(toTextNodePos(deletedID));
}
const pbInsertedIDs = [];
const insertedIDs = editReverseOperation.getInsertedIDs();
for (const insertedID of insertedIDs) {
pbInsertedIDs.push(toTextNodePos(insertedID));
}
pbEditReverseOperation.setDeletedIdsList(pbDeletedIDs);
pbEditReverseOperation.setInsertedIdsList(pbInsertedIDs);

const pbAttributes = pbEditReverseOperation.getAttributesMap();
for (const [key, value] of editReverseOperation.getAttributes()) {
pbAttributes.set(key, value);
}
pbEditReverseOperation.setExecutedAt(
toTimeTicket(editReverseOperation.getExecutedAt()),
);
pbOperation.setEditReverse(pbEditReverseOperation);
} else if (operation instanceof StyleOperation) {
const styleOperation = operation as StyleOperation;
const pbStyleOperation = new PbOperation.Style();
Expand Down Expand Up @@ -1066,15 +1097,42 @@ function fromOperations(pbOperations: Array<PbOperation>): Array<Operation> {
pbEditOperation!.getAttributesMap().forEach((value, key) => {
attributes.set(key, value);
});
operation = EditOperation.create(
fromTimeTicket(pbEditOperation!.getParentCreatedAt())!,
fromTextNodePos(pbEditOperation!.getFrom()!),
fromTextNodePos(pbEditOperation!.getTo()!),
createdAtMapByActor,
pbEditOperation!.getContent(),
operation = EditOperation.create({
parentCreatedAt: fromTimeTicket(pbEditOperation!.getParentCreatedAt())!,
fromPos: fromTextNodePos(pbEditOperation!.getFrom()!),
toPos: fromTextNodePos(pbEditOperation!.getTo()!),
content: pbEditOperation!.getContent(),
attributes,
fromTimeTicket(pbEditOperation!.getExecutedAt())!,
);
executedAt: fromTimeTicket(pbEditOperation!.getExecutedAt())!,
maxCreatedAtMapByActor: createdAtMapByActor,
});
} else if (pbOperation.hasEditReverse()) {
const pbEditReverseOperation = pbOperation.getEditReverse();
const attributes = new Map();
pbEditReverseOperation!.getAttributesMap().forEach((value, key) => {
attributes.set(key, value);
});

const pbDeletedIDs = pbEditReverseOperation!.getDeletedIdsList()!;
const deletedIDs = [];
for (const pbDeletedID of pbDeletedIDs) {
deletedIDs.push(fromTextNodePos(pbDeletedID));
}
const pbInsertedIDs = pbEditReverseOperation!.getInsertedIdsList()!;
const insertedIDs = [];
for (const pbInsertedID of pbInsertedIDs) {
insertedIDs.push(fromTextNodePos(pbInsertedID));
}

operation = EditReverseOperation.create({
parentCreatedAt: fromTimeTicket(
pbEditReverseOperation!.getParentCreatedAt(),
)!,
deletedIDs,
insertedIDs,
attributes,
executedAt: fromTimeTicket(pbEditReverseOperation!.getExecutedAt())!,
});
} else if (pbOperation.hasStyle()) {
const pbStyleOperation = pbOperation.getStyle();
const createdAtMapByActor = new Map();
Expand Down
8 changes: 8 additions & 0 deletions src/api/yorkie/v1/resources.proto
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ message Operation {
map<string, string> attributes = 4;
TimeTicket executed_at = 5;
}
message EditReverse {
TimeTicket parent_created_at = 1;
repeated TextNodePos deleted_ids = 2;
repeated TextNodePos inserted_ids = 3;
TimeTicket executed_at = 4;
map<string, string> attributes = 5;
}

oneof body {
Set set = 1;
Expand All @@ -145,6 +152,7 @@ message Operation {
Increase increase = 8;
TreeEdit tree_edit = 9;
TreeStyle tree_style = 10;
EditReverse edit_reverse = 11;
}
}

Expand Down
50 changes: 50 additions & 0 deletions src/api/yorkie/v1/resources_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ export class Operation extends jspb.Message {
hasTreeStyle(): boolean;
clearTreeStyle(): Operation;

getEditReverse(): Operation.EditReverse | undefined;
setEditReverse(value?: Operation.EditReverse): Operation;
hasEditReverse(): boolean;
clearEditReverse(): Operation;

getBodyCase(): Operation.BodyCase;

serializeBinary(): Uint8Array;
Expand All @@ -215,6 +220,7 @@ export namespace Operation {
increase?: Operation.Increase.AsObject,
treeEdit?: Operation.TreeEdit.AsObject,
treeStyle?: Operation.TreeStyle.AsObject,
editReverse?: Operation.EditReverse.AsObject,
}

export class Set extends jspb.Message {
Expand Down Expand Up @@ -627,6 +633,49 @@ export namespace Operation {
}


export class EditReverse extends jspb.Message {
getParentCreatedAt(): TimeTicket | undefined;
setParentCreatedAt(value?: TimeTicket): EditReverse;
hasParentCreatedAt(): boolean;
clearParentCreatedAt(): EditReverse;

getDeletedIdsList(): Array<TextNodePos>;
setDeletedIdsList(value: Array<TextNodePos>): EditReverse;
clearDeletedIdsList(): EditReverse;
addDeletedIds(value?: TextNodePos, index?: number): TextNodePos;

getInsertedIdsList(): Array<TextNodePos>;
setInsertedIdsList(value: Array<TextNodePos>): EditReverse;
clearInsertedIdsList(): EditReverse;
addInsertedIds(value?: TextNodePos, index?: number): TextNodePos;

getExecutedAt(): TimeTicket | undefined;
setExecutedAt(value?: TimeTicket): EditReverse;
hasExecutedAt(): boolean;
clearExecutedAt(): EditReverse;

getAttributesMap(): jspb.Map<string, string>;
clearAttributesMap(): EditReverse;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): EditReverse.AsObject;
static toObject(includeInstance: boolean, msg: EditReverse): EditReverse.AsObject;
static serializeBinaryToWriter(message: EditReverse, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): EditReverse;
static deserializeBinaryFromReader(message: EditReverse, reader: jspb.BinaryReader): EditReverse;
}

export namespace EditReverse {
export type AsObject = {
parentCreatedAt?: TimeTicket.AsObject,
deletedIdsList: Array<TextNodePos.AsObject>,
insertedIdsList: Array<TextNodePos.AsObject>,
executedAt?: TimeTicket.AsObject,
attributesMap: Array<[string, string]>,
}
}


export enum BodyCase {
BODY_NOT_SET = 0,
SET = 1,
Expand All @@ -639,6 +688,7 @@ export namespace Operation {
INCREASE = 8,
TREE_EDIT = 9,
TREE_STYLE = 10,
EDIT_REVERSE = 11,
}
}

Expand Down
Loading
Loading