Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 45 additions & 10 deletions src/static/js/Changeset.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,9 @@ exports.mergingOpAssembler = () => {
// ops immediately after it.
let bufOpAdditionalCharsAfterNewline = 0;

/**
* @param {boolean} [isEndDocument]
*/
const flush = (isEndDocument) => {
if (!bufOp.opcode) return;
if (isEndDocument && bufOp.opcode === '=' && !bufOp.attribs) {
Expand Down Expand Up @@ -676,6 +679,7 @@ exports.stringIterator = (str) => {
*/
exports.stringAssembler = () => ({
_str: '',
clear() { this._str = ''; },
/**
* @param {string} x -
*/
Expand Down Expand Up @@ -802,7 +806,7 @@ class TextLinesMutator {

/**
* Indicates if curLine is already in the splice. This is necessary because the last element in
* curSplice is curLine when this line is currently worked on (e.g. when skipping are inserting).
* curSplice is curLine when this line is currently worked on (e.g. when skipping or inserting).
*
* TODO(doc) why aren't removals considered?
*
Expand Down Expand Up @@ -830,7 +834,7 @@ class TextLinesMutator {
* It will skip some newlines by putting them into the splice.
*
* @param {number} L -
* @param {boolean} includeInSplice - indicates if attributes are present
* @param {boolean} includeInSplice - Indicates that attributes are present.
*/
skipLines(L, includeInSplice) {
if (!L) return;
Expand Down Expand Up @@ -959,7 +963,7 @@ class TextLinesMutator {
/** @type {string} */
const theLine = this._curSplice[sline];
const lineCol = this._curCol;
// insert the first new line
// Insert the chars up to `curCol` and the first new line.
this._curSplice[sline] = theLine.substring(0, lineCol) + newLines[0];
this._curLine++;
newLines.splice(0, 1);
Expand All @@ -975,9 +979,8 @@ class TextLinesMutator {
this._curLine += newLines.length;
}
} else {
// 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)
// 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).
const sline = this._putCurLineInSplice();
if (!this._curSplice[sline]) {
const err = new Error(
Expand Down Expand Up @@ -1276,6 +1279,13 @@ exports.applyToAttribution = (cs, astr, pool) => {
return applyZip(astr, unpacked.ops, (op1, op2) => slicerZipperFunc(op1, op2, pool));
};

/**
* Applies a changeset to an array of attribute lines.
*
* @param {string} cs - The encoded changeset.
* @param {Array<string>} lines - Attribute lines. Modified in place.
* @param {AttributePool} pool - Attribute pool.
*/
exports.mutateAttributionLines = (cs, lines, pool) => {
const unpacked = exports.unpack(cs);
const csOps = exports.deserializeOps(unpacked.ops);
Expand All @@ -1285,26 +1295,47 @@ exports.mutateAttributionLines = (cs, lines, pool) => {
// treat the attribution lines as text lines, mutating a line at a time
const mut = new TextLinesMutator(lines);

/** @type {?Generator<Op>} */
/**
* The Ops in the current line from `lines`.
*
* @type {?Generator<Op>}
*/
let lineOps = null;
let lineOpsNext = null;

const lineOpsHasNext = () => lineOpsNext && !lineOpsNext.done;
/**
* Returns false if we are on the last attribute line in `lines` and there is no additional op in
* that line.
*
* @returns {boolean} True if there are more ops to go through.
*/
const isNextMutOp = () => lineOpsHasNext() || mut.hasMore();

/**
* @returns {Op} The next Op from `lineIter`. If there are no more Ops, `lineIter` is reset to
* iterate over the next line, which is consumed from `mut`. If there are no more lines,
* returns a null Op.
*/
const nextMutOp = () => {
if (!lineOpsHasNext() && mut.hasMore()) {
// There are more attribute lines in `lines` to do AND either we just started so `lineIter` is
// still null or there are no more ops in current `lineIter`.
const line = mut.removeLines(1);
lineOps = exports.deserializeOps(line);
lineOpsNext = lineOps.next();
}
if (!lineOpsHasNext()) return new Op();
if (!lineOpsHasNext()) return new Op(); // No more ops and no more lines.
const op = lineOpsNext.value;
lineOpsNext = lineOps.next();
return op;
};
let lineAssem = null;

/**
* Appends an op to `lineAssem`. In case `lineAssem` includes one single newline, adds it to the
* `lines` mutator.
*/
const outputMutOp = (op) => {
if (!lineAssem) {
lineAssem = exports.mergingOpAssembler();
Expand All @@ -1321,25 +1352,29 @@ exports.mutateAttributionLines = (cs, lines, pool) => {
let attOp = new Op();
while (csOp.opcode || !csOpsNext.done || attOp.opcode || isNextMutOp()) {
if (!csOp.opcode && !csOpsNext.done) {
// coOp done, but more ops in cs.
csOp = csOpsNext.value;
csOpsNext = csOps.next();
}
if (!csOp.opcode && !attOp.opcode && !lineAssem && !lineOpsHasNext()) {
break; // done
} else if (csOp.opcode === '=' && csOp.lines > 0 && !csOp.attribs && !attOp.opcode &&
!lineAssem && !lineOpsHasNext()) {
// skip multiple lines; this is what makes small changes not order of the document size
// Skip multiple lines without attributes; this is what makes small changes not order of the
// document size.
mut.skipLines(csOp.lines);
csOp.opcode = '';
} else if (csOp.opcode === '+') {
const opOut = copyOp(csOp);
if (csOp.lines > 1) {
// Copy the first line from `csOp` to `opOut`.
const firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex;
csOp.chars -= firstLineLen;
csOp.lines--;
opOut.lines = 1;
opOut.chars = firstLineLen;
} else {
// Either one or no newlines in '+' `csOp`, copy to `opOut` and reset `csOp`.
csOp.opcode = '';
}
outputMutOp(opOut);
Expand Down Expand Up @@ -1763,7 +1798,7 @@ exports.copyAText = (atext1, atext2) => {
};

/**
* Convert AText to a series of operations.
* Convert AText to a series of operations. Strips final newline.
*
* @param {AText} atext - The AText to convert.
* @yields {Op}
Expand Down
2 changes: 1 addition & 1 deletion src/tests/frontend/easysync-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ exports.randomMultiline = randomMultiline;

const randomStringOperation = (numCharsLeft) => {
let result;
switch (randInt(9)) {
switch (randInt(11)) {
case 0:
{
// insert char
Expand Down
132 changes: 132 additions & 0 deletions src/tests/frontend/specs/easysync-assembler.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const Changeset = require('../../../static/js/Changeset');
const {poolOrArray} = require('../easysync-helper.js');

describe('easysync-assembler', function () {
it('opAssembler', async function () {
Expand All @@ -18,6 +19,137 @@ describe('easysync-assembler', function () {
expect(assem.toString()).to.equal(x);
});

it('smartOpAssembler ignore additional pure keeps (no attributes)', async function () {
const x = '-c*3*4+6|1+1=5';
const iter = Changeset.opIterator(x);
const assem = Changeset.smartOpAssembler();
while (iter.hasNext()) assem.append(iter.next());
assem.endDocument();
expect(assem.toString()).to.equal('-c*3*4+6|1+1');
});

it('smartOpAssembler merge consecutive + ops without multiline', async function () {
const x = '-c*3*4+6*3*4+1*3*4+9=5';
const iter = Changeset.opIterator(x);
const assem = Changeset.smartOpAssembler();
while (iter.hasNext()) assem.append(iter.next());
assem.endDocument();
expect(assem.toString()).to.equal('-c*3*4+g');
});

it('smartOpAssembler merge consecutive + ops with multiline', async function () {
const x = '-c*3*4+6*3*4|1+1*3*4|9+f*3*4+k=5';
const iter = Changeset.opIterator(x);
const assem = Changeset.smartOpAssembler();
while (iter.hasNext()) assem.append(iter.next());
assem.endDocument();
expect(assem.toString()).to.equal('-c*3*4|a+m*3*4+k');
});

it('smartOpAssembler merge consecutive - ops without multiline', async function () {
const x = '-c-6-1-9=5';
const iter = Changeset.opIterator(x);
const assem = Changeset.smartOpAssembler();
while (iter.hasNext()) assem.append(iter.next());
assem.endDocument();
expect(assem.toString()).to.equal('-s');
});

it('smartOpAssembler merge consecutive - ops with multiline', async function () {
const x = '-c-6|1-1|9-f-k=5';
const iter = Changeset.opIterator(x);
const assem = Changeset.smartOpAssembler();
while (iter.hasNext()) assem.append(iter.next());
assem.endDocument();
expect(assem.toString()).to.equal('|a-y-k');
});

it('smartOpAssembler merge consecutive = ops without multiline', async function () {
const x = '-c*3*4=6*2*4=1*3*4=f*3*4=2*3*4=a=k=5';
const iter = Changeset.opIterator(x);
const assem = Changeset.smartOpAssembler();
while (iter.hasNext()) assem.append(iter.next());
assem.endDocument();
expect(assem.toString()).to.equal('-c*3*4=6*2*4=1*3*4=r');
});

it('smartOpAssembler merge consecutive = ops with multiline', async function () {
const x = '-c*3*4=6*2*4|1=1*3*4|9=f*3*4|2=2*3*4=a*3*4=1=k=5';
const iter = Changeset.opIterator(x);
const assem = Changeset.smartOpAssembler();
while (iter.hasNext()) assem.append(iter.next());
assem.endDocument();
expect(assem.toString()).to.equal('-c*3*4=6*2*4|1=1*3*4|b=h*3*4=b');
});

it('smartOpAssembler ignore + ops with ops.chars === 0', async function () {
const x = '-c*3*4+6*3*4+0*3*4+1+0*3*4+1';
const iter = Changeset.opIterator(x);
const assem = Changeset.smartOpAssembler();
while (iter.hasNext()) assem.append(iter.next());
assem.endDocument();
expect(assem.toString()).to.equal('-c*3*4+8');
});

it('smartOpAssembler ignore - ops with ops.chars === 0', async function () {
const x = '-c-6-0-1-0-1';
const iter = Changeset.opIterator(x);
const assem = Changeset.smartOpAssembler();
while (iter.hasNext()) assem.append(iter.next());
assem.endDocument();
expect(assem.toString()).to.equal('-k');
});

it('smartOpAssembler append + op with text', async function () {
const assem = Changeset.smartOpAssembler();
const pool = poolOrArray([
'attr1,1',
'attr2,2',
'attr3,3',
'attr4,4',
'attr5,5',
]);

assem.appendOpWithText('+', 'test', '*3*4*5', pool);
assem.appendOpWithText('+', 'test', '*3*4*5', pool);
assem.appendOpWithText('+', 'test', '*1*4*5', pool);
assem.endDocument();
expect(assem.toString()).to.equal('*3*4*5+8*1*4*5+4');
});

it('smartOpAssembler append + op with multiline text', async function () {
const assem = Changeset.smartOpAssembler();
const pool = poolOrArray([
'attr1,1',
'attr2,2',
'attr3,3',
'attr4,4',
'attr5,5',
]);

assem.appendOpWithText('+', 'test\ntest', '*3*4*5', pool);
assem.appendOpWithText('+', '\ntest\n', '*3*4*5', pool);
assem.appendOpWithText('+', '\ntest', '*1*4*5', pool);
assem.endDocument();
expect(assem.toString()).to.equal('*3*4*5|3+f*1*4*5|1+1*1*4*5+4');
});

it('smartOpAssembler clear should empty internal assemblers', async function () {
const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
const iter = Changeset.opIterator(x);
const assem = Changeset.smartOpAssembler();
assem.append(iter.next());
assem.append(iter.next());
assem.append(iter.next());
assem.clear();
assem.append(iter.next());
assem.append(iter.next());
assem.clear();
while (iter.hasNext()) assem.append(iter.next());
assem.endDocument();
expect(assem.toString()).to.equal('-1+1*0+1=1-1+1|c=c-1');
});

describe('append atext to assembler', function () {
const testAppendATextToAssembler = (testId, atext, correctOps) => {
it(`testAppendATextToAssembler#${testId}`, async function () {
Expand Down
9 changes: 9 additions & 0 deletions src/tests/frontend/specs/easysync-mutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,15 @@ describe('easysync-mutations', function () {

testMutateTextLines(1, 'Z:4<1|1-2-1|1+1+1$\nc', ['a\n', 'b\n'], ['\n', 'c\n']);
testMutateTextLines(2, 'Z:4>0|1-2-1|2+3$\nc\n', ['a\n', 'b\n'], ['\n', 'c\n', '\n']);

it('mutate keep only lines', async function () {
const lines = ['1\n', '2\n', '3\n', '4\n'];
const result = lines.slice();
const cs = 'Z:8>0*0|1=2|2=2';

Changeset.mutateTextLines(cs, lines);
expect(result).to.eql(lines);
});
});

describe('mutate attributions', function () {
Expand Down