Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Undo/redo support for new editor #3267

Merged
merged 7 commits into from
Aug 2, 2019
Merged

Undo/redo support for new editor #3267

merged 7 commits into from
Aug 2, 2019

Conversation

bwindels
Copy link
Contributor

@bwindels bwindels commented Aug 1, 2019

Fixes element-hq/element-web#9831

The algorithm when to push history steps might still need a bit of fine-tuning. Right now it always pushes on every change, apart from when typing (and not overwriting), then it pushes a step on every 5th typed character.

editor-history

@bwindels bwindels requested a review from a team August 1, 2019 09:36
@jryans jryans requested review from jryans and removed request for a team August 1, 2019 12:59
Copy link
Collaborator

@jryans jryans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this! A few questions and minor changes first.

if (diff.added) {
// only append after every 5th keystroke while typing
this._newlyTypedCharCount += diff.added.length;
return this._newlyTypedCharCount > 5;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend extracting this magic number out to a constant at the top of the module with a comment explaining it's purpose there.

return true;
}
if (diff.added) {
// only append after every 5th keystroke while typing
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why every 5 chars? Would it make more sense to append at word boundaries instead, or is that too complex?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be more complex, but probably worthwhile change. Will have a look 👍

this._parts = serializedParts.map(p => this._partCreator.deserializePart(p));
this._updateCallback(caret, inputType);
}

update(newValue, inputType, caret) {
const diff = this._diff(newValue, inputType, caret);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we're diffing on every keystroke I guess? How expensive is that? Does this processing make the input experience feel sluggish at all? (I assume it shouldn't be sluggish at least, since the browser has already displayed the new character before the handler runs...?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this need a diffing algorithm rather than just snapshotting the editor state on every word boundary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity, the diffing is part of how the editor works, and is unrelated to the undo support.

The diffing algorithm is very basic, and assumes there is only one change around the caret position, so this should be very inexpensive. See editor/diff.js for details. The most expensive part is probably to turn the content-editable content into a single string (with some logic, see getCaretOffsetAndText) on every keystroke. Some preliminary profiling shows that we can probably make some improvements still, and there is an item on element-hq/element-web#9866 for that.

As for the reason to use diffing; the alternative would be to recreate the model on every keystroke based on the DOM of the editor. Without considering the runtime cost/benefits of this, it would not allow us preserve state for a given section of the editor, like the part type, or the userid for a pill.

So on every keystroke, the editor dom is turned into a string, and the model updated: the model turns its current state into text, and compares the current and new text value. The difference between both is then applied to the existing parts of the model state, applying validation logic in the parts.

The benefit of this is that we can use the input event, which is broadly supported, to find changes in the editor. We don't have to rely on keyboard events, which poorly relate to text input or changes.

Once the parts of the model are updated, the DOM of the editor is then reconciled with the new model state, see renderModel in editor/render.js for this. If the model didn't reject the input and didn't make any additional changes, this won't make any changes to the DOM at all, and should thus be fairly efficient.

I should probably document all of this somewhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, ok. i would definitely put all this into a massive /**/ comment at the top of the editor file, or possibly even docs/brunoEditor.md or whatever we're calling this ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#3272 adds some documentation.

@bwindels bwindels force-pushed the bwindels/editor-history branch from b9dc45a to 07b2e51 Compare August 1, 2019 14:27
@bwindels
Copy link
Contributor Author

bwindels commented Aug 2, 2019

I thought the word boundary was going to be more complicated because right now I can only add an undo step including the last change, and constantly serializing the model might slow down things a bit. This means the undo step will include the last word boundary character, but in practice this seems fine.

editor-history-word-boundaries

@bwindels bwindels requested a review from jryans August 2, 2019 09:36
Copy link
Collaborator

@jryans jryans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay, let's try it out! 😁

@bwindels bwindels merged commit ce32c9f into develop Aug 2, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Message editing: support undo/redo in new composer
3 participants