Getting author ownership right in a collaborative editor

In this blog post I’m going to explain the complexities of getting content ownership in a document right. All popular collaborative editors get this wrong, some more drastically than others.

Knowing who owns a certain piece of text in a document is an extremely important part of a collaborative editor, I will explain why later in the post.

Let’s start with a basic example.

John writes.

My dog ate the tree

Now Dave swaps the words tree and dog around. We get

My tree ate the dog

The actual sentiment of the sentence has changed a lot but that wasn’t John’s intention yet if we look at the sentence in all collaborative Editors there is no representation that Dave modified the text.

As you can imagine this is a huge problem as far as sentiment analysis, Etherpad suffers from this too. Sentiment is distorted without the viewer being aware, imagine now we are working on a legal document and we replace the word tree and dog with friend and zombie…

As text is modified in Etherpad new attributes IE underline/bold/strikethrough aren’t made clear to the viewer. IE if John writes

Hello world

Then Dave applies a strikethrough on this text we have no recollection that Dave made this modification. The viewer is under the impression that John struck this this piece of text, this is misleading.

Undo also falls into the trap of modifying someone elses text without it being clear, the same applies as the basic example, sentiment can be modified without the original Authors consent.

So what’s the solution?

Boldly going..

At current each piece of text in Etherpad has an author owner, I propose that on attribute modification (IE bold/strikethrough) additionally each additional author exists in an “editors” array. The original author would still be listed as the “creator/author”.

To the replicator..

In the “tree ate the dog” example the original author(John) should still be maintained but additionally(this is the new bit) the editor(Dave) should be added to the “editors” array. Part of Etherpad’s design is that as text is Copy/Pasted the original Author is maintained, this is healthy and by adding the editors array we should be able to identify that Dave has modified John’s content.

Undoing the deed

John might delete Dave’s text then hit Undo but then we have to decide if the undone text is owned by Dave, John or if it’s owned by Dave with a modification from John. Again adding Dave as an editor of the content would make it clear that a second author has modified the content.

Using git bisect to debug Etherpad issues

Sometimes stuff gets broken due to new commits, we need to know which commit broke functionality.

To do this

git pull
git checkout develop

Ensure the bug exists then

git checkout master

Ensure the bug doesn’t exist. If it does checkout older versions IE git checkout release/1.2.8
Next begin the bisect process..

git bisect start

Tell bisect that master is fine

git bisect good

Tell bisect that develop is bad

git checkout develop
git bisect bad

Bisect will then deliver you a commit state, test this new version..

bin/run.sh

Test, Control C.. Did it work? If so…

git bisect good

Did it not work? If so..

git bisect bad

Rinse repeat ‘git bisect good’ and/or ‘git bisect bad’ until it delivers you with a commit SHA then create an issue including the details from the SHA.

Basically bisect will tell you which commit introduced the bug.

Script for checking out pull request

I often have to test specific pull requests and tanks to this post on how to checkout git pull requests I was able to quickly bash together a script

I wrote this to ~/checkoutPR.sh

echo "Getting Pull request #$1"
git fetch origin pull/$1/head:pr-$1
git checkout pr-$1

then

chmod 775 ~/checkoutPR.sh

Then jump into the repo folder
Grab the Pull request # from the URL, replacing %pullrqeuestnumber% with the pull request #

~/checkoutPR.sh %pullrqeuestnumber%

Simple but useful.

The output is something like this

jose@debian:~/etherpad-lite$ ~/./checkoutPR.sh 1672
Getting Pull request #1672
remote: Counting objects: 142, done.
remote: Compressing objects: 100% (43/43), done.
remote: Total 124 (delta 101), reused 104 (delta 81)
Receiving objects: 100% (124/124), 13.58 KiB, done.
Resolving deltas: 100% (101/101), completed with 15 local objects.
From github.com:ether/etherpad-lite
 * [new branch]      refs/pull/1672/head -> pr-1672
Switched to branch 'pr-1672'

Doing Collaborative editing viewport scrolling right

Getting scrolling in an editor right is a complex task with various Gotchas. Getting scrolling right is in a collaborative editing environment is fraught with even more perils.   Most editors, both collaborative and non collaborative get scrolling wrong, yours probably does too.   But fear not! In this post I’m going to explain some edge case behaviours and how you should go about handling them.

I hold WordPress in really high regard yet WordPress as an example gets this very wrong(for example hitting page down in your editor will lose your caret if your document is greater than the height of your editor), TinyMCE is probably not to blame for this, WYSIWYG editors on the web in general are broken.

The behaviour for how editors scroll with documents has become a relatively normal experience. When you hit enter you expect to create a new line and for your browser to continue bringing that newline into your viewport.

When we talk about a viewport we mean something like this

  --------------
  |  document  |
|----------------|
|                |
|    viewport    |
|                |
|----------------|
  |            |
  --------------

As you can see the viewport shows a portion(or if the document is smaller than the viewport the whole) of the document and as a newline is created the viewport is moved down by the pixel height of the new line.  If you are a WYSIWYG or Etherpad user think of it as the large white panel where you can edit text.

So we know that moving the Viewport Offset Top to the right location on a document in a non collaborative environment is really easy, yet there are some behaviours we take for granted.

Taken for granted

Using left arrow key moves the caret one position left until it hits the beginning of a row then it goes to the last position on the row above

Using the up/down arrows move our caret up, maintaining X(IE 5) position, when the caret hits the Viewpoint offset minus the Above line height the viewport is moved by minus the Above line height. Similar happens for the down arrow. If there is no line X content the caret X position is 0. 0 is not retained though, if the user presses up/down again the caret maintains it’s original trajectory, IE 5

Using the page up/down arrows move our caret up to the first visible line in the viewport. Hitting page up again will take the current viewport height and move the first fully visible line in the new viewport position. Page down does similar.

Page up:

|----------------|
|    viewport    |
|                |
|----------------| 
  |  document  |
  --------------

Page down:

  --------------
  |  document  |
  |            |
|----------------|
|                |
|    viewport    |
|                |
|----------------|

If your editor gets these wrong then you have serious problems.

Gotchas

It’s at this point that collaborative editing throws some gotchas in.

Ever changing line heights above content

As you type this happens..
State a:

  --------------
  |  document  |
  |            |
|----------------|
|                |
|    viewport    |
|                |
|----------------|

State b:

  --------------
  |  document  |
  |            |
  |            |
|----------------|
|                |
|    viewport    |
|                |
|----------------|

As you can see here another user has edited the content above our viewport which has left our viewport moved further down the document. This isn’t the default behaviour with collaborative editors, you will have to deal with this edge case specifically.

The same problem exists if content is resized/removed above your content, again knowing the caret position at modification is how we solve this however there is another gotcha..

The author is reading

So your author is reading content 5 lines above your current caret position and someone adds new content at the top of the document, you will not want to move the users caret position to the new location. I call this problem the “viewport war” problem.

How we solve this problem

Part 1: Is the caret in viewport range at all(If not we can assume the user is reading text elsewhere in the pad so don’t need to do anything)?

Part 2: If a new change above would modify the viewport to the point where the caret would no longer be visible for the user then always put the Offset Top at Caret Offset Top – (ViewPort height / 2). This means that the Caret is in the middle of the Y portion of the Viewport.

Highlighting

During highlight we should never allow the viewport to be moved by an edit event, only the user should be able to change it however this poses a new problem.. Imagine we are highlighting lines 5 to 10 and someone deletes lines 0, our editor would now look something like this..

|----------------|
| |------------| |
| |  document  | |
| |            | |
|-|            |-|
|-|------------|-|

Note the line above the document, the document is now below the top of our viewport.

How we solve this problem

This is another issue you will have to deal with in your editor. The best way is to basically stop the viewport moving if the content is being highlighted, this means that you allow the document (note we say document and not viewport) Offset Top to actually be greater than 0.

So to summarize

* Don’t allow edits above the viewport to move the viewport out of the visible caret location unless the caret is already outside the visible caret location.
* Don’t allow user changes to move viewport when author is highlighting

Anyway I wrote this so I can refactor the caret and viewport in Etherpad, a free and open source really-real time collaborative editor.