-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
Fix textLinesMutator when dealing with insertions/deletions at the end of pad #5253
base: develop
Are you sure you want to change the base?
Changes from all commits
97e9ea3
fe5d438
01b04a6
f6235ce
6d224f5
f8b41ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -908,20 +908,24 @@ class TextLinesMutator { | |
let removed = ''; | ||
if (this._isCurLineInSplice()) { | ||
if (this._curCol === 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about line markers? If the first character is a line marker and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I don't understand, but the ops that are applied to lines shouldn't care about lineMarker's special meaning in the editor. ie lineMarker is special in the editor, because it's not rendered and it changes the "meaning" of the whole line, but when it comes to Changeset.js whatever attributes apply to the lineMarker are only applied to this single, "real" character. So we should not treat |
||
// First line to be removed is in splice. | ||
removed = this._curSplice[this._curSplice.length - 1]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line makes the following assumptions:
It's not immediately obvious to me that either assumption is guaranteed to be true. Examples:
Maybe those assumptions are guaranteed to be true due to (undocumented?) invariants. If so, it would be good to assert them here. |
||
this._curSplice.length--; | ||
// Next lines to be removed are not in splice. | ||
removed += nextKLinesText(L - 1); | ||
this._curSplice[1] += L - 1; | ||
} else { | ||
removed = nextKLinesText(L - 1); | ||
this._curSplice[1] += L - 1; | ||
const sline = this._curSplice.length - 1; | ||
removed = this._curSplice[sline].substring(this._curCol) + removed; | ||
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + | ||
this._linesGet(this._curSplice[0] + this._curSplice[1]); | ||
this._curSplice[1] += 1; | ||
// Is there a line left? | ||
const remaining = this._linesGet(this._curSplice[0] + this._curSplice[1]) || ''; | ||
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + remaining; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js
index 5e0bc34fc..defc08852 100644
--- a/src/static/js/Changeset.js
+++ b/src/static/js/Changeset.js
@@ -918,11 +918,23 @@ class TextLinesMutator {
removed = nextKLinesText(L - 1);
this._curSplice[1] += L - 1;
const sline = this._curSplice.length - 1;
- removed = this._curSplice[sline].substring(this._curCol) + removed;
- // Is there a line left?
- const remaining = this._linesGet(this._curSplice[0] + this._curSplice[1]) || '';
- this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + remaining;
- this._curSplice[1] += remaining ? 1 : 0;
+ removed = this._curSplice[sline].slice(this._curCol) + removed;
+ this._curSplice[sline] = this._curSplice[sline].slice(0, this._curCol);
+ // The current line's newline was deleted, which means that the line after the splice (if
+ // there is one) should be joined with what remains of the current line.
+ const n = this._curSplice[0] + this._curSplice[1];
+ if (n < this._linesLength()) {
+ // There is a line that can be joined with the current line.
+ // TODO: What if the joined line starts with a line marker?
+ // TODO: What if the joined line has line attributes that differ from the current line's
+ // line attributes?
+ this._curSplice[sline] += this._linesGet(n);
+ this._curSplice[1] += 1;
+ } else {
+ // There is no line that can be joined with the current line. The current line's newline
+ // was removed, but all lines must end with a newline so add it back.
+ this._curSplice[sline] += '\n';
+ }
}
} else {
// Nothing that is removed is in splice. Implies curCol === 0. |
||
this._curSplice[1] += remaining ? 1 : 0; | ||
} | ||
} else { | ||
// Nothing that is removed is in splice. Implies curCol === 0. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be good to assert that |
||
removed = nextKLinesText(L); | ||
this._curSplice[1] += L; | ||
} | ||
|
@@ -973,17 +977,24 @@ class TextLinesMutator { | |
this._curLine += newLines.length; | ||
// insert the remaining chars from the "old" line (e.g. the line we were in | ||
// when we started to insert new lines) | ||
this._curSplice.push(theLine.substring(lineCol)); | ||
this._curCol = 0; // TODO(doc) why is this not set to the length of last line? | ||
const remaining = theLine.substring(lineCol); | ||
if (remaining !== '') this._curSplice.push(remaining); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If Also, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is not necessarly a newline at every line. Etherpad uses it with lines that always end with newlines (e.g. rep.lines), except for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do understand what you wrote in the first paragraph, though. If there is a newline, we would never go "beyond" that. Let me test |
||
this._curCol = 0; | ||
} else { | ||
this._curSplice.push(...newLines); | ||
this._curLine += newLines.length; | ||
} | ||
} else { | ||
} else if (!this.hasMore()) { | ||
// There are no additional lines. Although the line is put into splice, curLine is not | ||
// increased because there may be more chars in the line (newline is not reached). | ||
// increased because there may be more chars in the line (newline is not reached). We are | ||
// inserting at the end of lines. curCol is 0 as curLine is not in splice. | ||
this._curSplice.push(text); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is right. If my understanding is correct, each entry in I think the actual bug is inside diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js
index 5e0bc34fc..930a5e187 100644
--- a/src/static/js/Changeset.js
+++ b/src/static/js/Changeset.js
@@ -823,11 +823,18 @@ class TextLinesMutator {
* @returns {number} the index of the added line in curSplice
*/
_putCurLineInSplice() {
- if (!this._isCurLineInSplice()) {
- this._curSplice.push(this._linesGet(this._curSplice[0] + this._curSplice[1]));
- this._curSplice[1]++;
+ assert(this._inSplice, 'not in splice');
+ assert(this._curLine >= this._curSplice[0], 'curLine is before splice start');
+ while (this._curSplice[0] + this._curSplice.length - 2 < this._curLine + 1) {
+ const n = this._curSplice[0] + this._curSplice[1];
+ if (n < this._linesLength()) {
+ this._curSplice.push(this._linesGet(n));
+ this._curSplice[1]++;
+ } else {
+ // No more lines available to add to the splice. Create a new line from scratch.
+ this._curSplice.push('\n');
+ }
}
- // TODO should be the same as this._curSplice.length - 1
return 2 + this._curLine - this._curSplice[0];
}
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for pointing to Let's make sure newlines are present at all times There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What should happen if an op removes the last line's newline but that op is immediately followed by another op that restores the newline? For example: const text = 'x\n';
const lines = [text];
const cs = '=1|1-1|1+1$\n'; // Keep a char, then remove a newline, then add a newline.
Changeset.mutateTextLines(cs, lines);
assert.equal(lines.join(''), 'x\n');
assert.equal(Changeset.applyToText(cs, text), 'x\n'); Similarly, what if an op adds text to the end of the document without a trailing newline, then the next op adds the newline: const text = 'x\n';
const lines = [text];
const cs = '|1=2+1|1+1$y\n'; // Keep existing text, add a char, then add a newline.
Changeset.mutateTextLines(cs, lines);
assert.equal(lines.join(''), 'x\ny\n');
assert.equal(Changeset.applyToText(cs, text), 'x\ny\n'); If we force newlines to always be present in each entry in |
||
this._curCol += text.length; | ||
} else { | ||
// insert text after curCol | ||
const sline = this._putCurLineInSplice(); | ||
if (!this._curSplice[sline]) { | ||
// TODO should never happen now | ||
const err = new Error( | ||
'curSplice[sline] not populated, actual curSplice contents is ' + | ||
`${JSON.stringify(this._curSplice)}. Possibly related to ` + | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if
this._curCol
is greater than or equal to the current line's length? In that case, shouldn't we setthis._curCol
to 0 and incrementthis._curLine
before this check? Or is that case not possible due to invariants?If invariants make that case impossible, I think it would be worthwhile to add an assert statement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
skip:
insert:
remove:
In cases [1] and [2] curCol calculation is safe, as a text string is given to the respective function (the term that's added to curCol is derived from from text).
Also after calling
close
or skipping over multiple L that don't have attributes, the splice is not active anymore and it's possible that curCol is > 0. So there are four possible "start" states:So for all 4 "start" states, I'll add coverage for [0] exceeding the number of chars in the line (ie a text of "test\n" and skipping over 6 chars so we end in the middle of the next line), followed by a call to removeLines.
If we want to assert I think it's better to do it where curCol is changed.