Handle tabs for range comments

Changes:
- wrapping in HL now adds cssClass instead of discarding previous ones.
- getLength accounts for tab tags correctly.
- generic splitNode, potentially should be moved into util.
- more tests.

Feature: Issue 3915
Change-Id: Id8a646a5de4fd702aa112678c039df9ff8dd8c0b
This commit is contained in:
Viktar Donich
2016-06-28 14:48:33 -07:00
parent b74a83162f
commit e88b612e29
2 changed files with 172 additions and 66 deletions

View File

@@ -327,7 +327,7 @@
},
/**
* Traverse diff content from right to left, call callback for each node.
* Traverse Element from right to left, call callback for each node.
* Stops if callback returns true.
*
* @param {!Node} startNode
@@ -338,7 +338,9 @@
var travelLeft = opt_flags && opt_flags.left;
var node = startNode;
while (node) {
if (node instanceof Element && node.tagName !== 'HL') {
if (node instanceof Element &&
node.tagName !== 'HL' &&
node.tagName !== 'SPAN') {
break;
}
var nextNode = travelLeft ? node.previousSibling : node.nextSibling;
@@ -360,7 +362,6 @@
node = node.firstChild;
var length = 0;
while (node) {
// Only measure Text nodes and <hl>
if (node instanceof Text || node.tagName == 'HL') {
length += this._getLength(node);
}
@@ -380,10 +381,16 @@
* @return {!Element} Wrapped node.
*/
_wrapInHighlight: function(node, cssClass) {
var hl = document.createElement('hl');
hl.className = cssClass;
Polymer.dom(node.parentElement).replaceChild(hl, node);
hl.appendChild(node);
var hl;
if (node.tagName === 'HL') {
hl = node;
hl.classList.add(cssClass);
} else {
hl = document.createElement('hl');
hl.className = cssClass;
Polymer.dom(node.parentElement).replaceChild(hl, node);
hl.appendChild(node);
}
return hl;
},
@@ -394,7 +401,7 @@
* @param {number} offset
* @return {!Text} Trailing Text Node.
*/
_splitText: function(node, offset) {
_splitTextNode: function(node, offset) {
if (node.textContent.match(REGEX_ASTRAL_SYMBOL)) {
// DOM Api for splitText() is broken for Unicode:
// https://mathiasbynens.be/notes/javascript-unicode
@@ -412,11 +419,42 @@
}
},
/**
* Split Node at offset.
* If Node is Element, it's cloned and the node at offset is split too.
*
* @param {!Node} node
* @param {number} offset
* @return {!Node} Trailing Node.
*/
_splitNode: function(element, offset) {
if (element instanceof Text) {
return this._splitTextNode(element, offset);
}
var tail = element.cloneNode(false);
element.parentElement.insertBefore(tail, element.nextSibling);
// Skip nodes before offset.
var node = element.firstChild;
while (node &&
this._getLength(node) <= offset ||
this._getLength(node) === 0) {
offset -= this._getLength(node);
node = node.nextSibling;
}
if (this._getLength(node) > offset) {
tail.appendChild(this._splitNode(node, offset));
}
while (node.nextSibling) {
tail.appendChild(node.nextSibling);
}
return tail;
},
/**
* Split Text Node and wrap it in hl with cssClass.
* Wraps trailing part after split, tailing one if opt_firstPart is true.
*
* @param {!Text} node
* @param {!Node} node
* @param {number} offset
* @param {string} cssClass
* @param {boolean=} opt_firstPart
@@ -426,10 +464,10 @@
return this._wrapInHighlight(node, cssClass);
} else {
if (opt_firstPart) {
this._splitText(node, offset);
this._splitNode(node, offset);
// Node points to first part of the Text, second one is sibling.
} else {
node = this._splitText(node, offset);
node = this._splitNode(node, offset);
}
return this._wrapInHighlight(node, cssClass);
}
@@ -467,29 +505,21 @@
if (startNode instanceof Text) {
startNode =
this._splitAndWrapInHighlight(startNode, startOffset, cssClass);
startContent.insertBefore(startNode, startNode.nextSibling);
// Edge case: single line, text node wraps the highlight.
if (isOneLine && this._getLength(startNode) > length) {
var extra = this._splitText(startNode.firstChild, length);
var extra = this._splitTextNode(startNode.firstChild, length);
startContent.insertBefore(extra, startNode.nextSibling);
startContent.normalize();
}
} else if (startNode.tagName == 'HL') {
if (!startNode.classList.contains(cssClass)) {
var hl = startNode;
startNode = this._splitAndWrapInHighlight(
startNode.firstChild, startOffset, cssClass);
startContent.insertBefore(startNode, hl.nextSibling);
// Edge case: single line, <hl> wraps the highlight.
if (isOneLine && this._getLength(startNode) > length) {
var trailingHl = hl.cloneNode(false);
trailingHl.appendChild(
this._splitText(startNode.firstChild, length));
startContent.insertBefore(trailingHl, startNode.nextSibling);
}
if (hl.textContent.length === 0) {
hl.remove();
// Should leave wrapping HL's content after the highlight.
if (isOneLine && startOffset + length < this._getLength(startNode)) {
this._splitNode(startNode, startOffset + length);
}
startNode =
this._splitAndWrapInHighlight(startNode, startOffset, cssClass);
}
} else {
startNode = null;
@@ -531,8 +561,7 @@
// Split text inside HL.
var hl = endNode;
endNode = this._splitAndWrapInHighlight(
endNode.firstChild, endOffset, cssClass, true);
endContent.insertBefore(endNode, hl);
endNode, endOffset, cssClass, true);
if (hl.textContent.length === 0) {
hl.remove();
}
@@ -562,19 +591,32 @@
// Grow starting highlight until endNode or end of line.
if (startNode && startNode != endNode) {
this._traverseContentSiblings(startNode.nextSibling, function(node) {
startNode.textContent += node.textContent;
node.remove();
var growStartHl = function(node) {
if (node instanceof Text || node.tagName === 'SPAN') {
startNode.appendChild(node);
} else if (node.tagName === 'HL') {
this._traverseContentSiblings(node.firstChild, growStartHl);
node.remove();
}
return node == endNode;
});
}.bind(this);
this._traverseContentSiblings(startNode.nextSibling, growStartHl);
startNode.normalize();
}
if (!isOneLine && endNode) {
var growEndHl = function(node) {
if (node instanceof Text || node.tagName === 'SPAN') {
endNode.insertBefore(node, endNode.firstChild);
} else if (node.tagName === 'HL') {
this._traverseContentSiblings(node.firstChild, growEndHl);
node.remove();
}
}.bind(this);
// Prepend text up to line start to the ending highlight.
this._traverseContentSiblings(endNode.previousSibling, function(node) {
endNode.textContent = node.textContent + endNode.textContent;
node.remove();
}, {left: true});
this._traverseContentSiblings(
endNode.previousSibling, growEndHl, {left: true});
endNode.normalize();
}
},

View File

@@ -42,21 +42,19 @@ limitations under the License.
<tr class="diff-row side-by-side" left-type="remove" right-type="add">
<td class="left lineNum" data-value="140">140</td>
<!-- Next tag is formatted to eliminate zero-length text nodes. -->
<td class="content remove lightHighlight">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a udiam, <hl>quid</hl> sit, quod <hl>Epicurum</hl><gr-diff-comment-thread>
<td class="content remove lightHighlight">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab withIndicator" style="tab-size:8;"></span></hl>udiam, <hl>quid</hl> sit, <span class="tab withIndicator" style="tab-size:8;"></span>quod <hl>Epicurum</hl><gr-diff-comment-thread>
[Yet another random diff thread content here]
</gr-diff-comment-thread></td>
<td class="right lineNum" data-value="120">120</td>
<td class="content add lightHighlight">nacti ,
<hl>,</hl>
sumus otiosum, audiam, sit, quod
</td>
<!-- Next tag is formatted to eliminate zero-length text nodes. -->
<td class="content add lightHighlight">nacti , <hl>,</hl> sumus <hl><span class="tab withIndicator" style="tab-size:8;"></span></hl> otiosum, <span class="tab withIndicator" style="tab-size:8;"></span> audiam, sit, quod</td>
</tr>
</tbody>
<tbody class="section both">
<tr class="diff-row side-by-side" left-type="both" right-type="both">
<td class="left lineNum" data-value="141"></td>
<td class="content both darkHighlight">nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;</td>
<td class="content both darkHighlight">nam et<hl><span class="tab withIndicator" style="tab-size:8;"> </span></hl>complectitur<span class="tab withIndicator" style="tab-size:8;"> </span>verbis, quod vult, et dicit plane, quod intellegam;</td>
<td class="right lineNum" data-value="130"></td>
<td class="content both darkHighlight">nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;</td>
</tr>
@@ -97,7 +95,7 @@ limitations under the License.
<td class="left lineNum" data-value="165"></td>
<td class="content both darkHighlight">in physicis, quibus maxime gloriatur, primum totus est alienus. Democritea dicit</td>
<td class="right lineNum" data-value="147"></td>
<td class="content both darkHighlight">in physicis, quibus maxime gloriatur, primum totus est alienus. Democritea dicit</td>
<td class="content both darkHighlight">in physicis, <hl><span class="tab withIndicator" style="tab-size:8;"> </span></hl> quibus maxime gloriatur, primum totus est alienus. Democritea dicit</td>
</tr>
</tbody>
@@ -222,7 +220,6 @@ limitations under the License.
});
});
test('renders lines in comment range on comment discard', function(done) {
element.fire('comment-discard', {
comment: {
@@ -314,10 +311,10 @@ limitations under the License.
var diff = element.querySelector('#diffTable');
var startContent =
diff.querySelector('.left.lineNum[data-value="138"] ~ .content');
var endContent =
diff.querySelector('.left.lineNum[data-value="141"] ~ .content');
var betweenContent =
diff.querySelector('.left.lineNum[data-value="140"] ~ .content');
var endContent =
diff.querySelector('.left.lineNum[data-value="141"] ~ .content');
var commentThread =
diff.querySelector('gr-diff-comment-thread');
var builder = {
@@ -333,7 +330,7 @@ limitations under the License.
startContent);
builder.getContentByLine.withArgs(141, 'left').returns(
endContent);
element._applyRangedHighlight('some', 138, 4, 141, 8, 'left');
element._applyRangedHighlight('some', 138, 4, 141, 28, 'left');
assert.instanceOf(startContent.childNodes[0], Text);
assert.equal(startContent.childNodes[0].textContent, '[14]');
assert.instanceOf(startContent.childNodes[1], Element);
@@ -342,14 +339,6 @@ limitations under the License.
assert.equal(startContent.childNodes[1].tagName, 'HL');
assert.equal(startContent.childNodes[1].className, 'some');
assert.instanceOf(endContent.childNodes[0], Element);
assert.equal(endContent.childNodes[0].textContent, 'nam et c');
assert.equal(endContent.childNodes[0].tagName, 'HL');
assert.equal(endContent.childNodes[0].className, 'some');
assert.instanceOf(endContent.childNodes[1], Text);
assert.equal(endContent.childNodes[1].textContent,
'omplectitur verbis, quod vult, et dicit plane, quod intellegam;');
assert.instanceOf(betweenContent.firstChild, Element);
assert.equal(betweenContent.firstChild.tagName, 'HL');
assert.equal(betweenContent.firstChild.className, 'some');
@@ -364,6 +353,22 @@ limitations under the License.
assert.strictEqual(betweenContent.querySelector('gr-diff-comment-thread'),
commentThread, 'Comment threads should be preserved.');
assert.instanceOf(endContent.childNodes[0], Element);
assert.equal(endContent.childNodes[0].textContent,
'nam et\tcomplectitur\tverbis, ');
assert.equal(endContent.childNodes[0].tagName, 'HL');
assert.equal(endContent.childNodes[0].className, 'some');
assert.instanceOf(endContent.childNodes[1], Text);
assert.equal(endContent.childNodes[1].textContent,
'quod vult, et dicit plane, quod intellegam;');
var endHl = endContent.querySelector('hl.some');
assert.equal(endHl.childNodes.length, 5);
var tabs = endHl.querySelectorAll('span.tab');
assert.equal(tabs.length, 2);
assert.equal(tabs[0].previousSibling.textContent, 'nam et');
assert.equal(tabs[1].previousSibling.textContent, 'complectitur');
assert.equal(tabs[1].nextSibling.textContent, 'verbis, ');
});
suite('single line ranges', function() {
@@ -394,10 +399,18 @@ limitations under the License.
assert.equal(content.firstChild.tagName, 'HL');
assert.equal(content.firstChild.className, 'some');
assert.equal(content.childNodes.length, 2);
assert.equal(content.firstChild.childNodes.length, 1);
assert.equal(content.firstChild.childNodes.length, 5);
assert.equal(content.firstChild.textContent,
'na💢ti te, inquit, sumus aliquando otiosum, certe a udiam, ' +
'quid sit, quod Epicurum');
var tabs = content.querySelectorAll('span.tab');
assert.equal(tabs.length, 2);
assert.strictEqual(tabs[1].previousSibling, tabs[0].nextSibling);
assert.equal(tabs[0].previousSibling.textContent,
'na💢ti te, inquit, sumus aliquando otiosum, certe a ');
assert.equal(tabs[1].previousSibling.textContent,
'udiam, quid sit, ');
assert.equal(tabs[1].nextSibling.textContent, 'quod Epicurum');
});
test('merging multiple other hls', function() {
@@ -406,7 +419,8 @@ limitations under the License.
assert.equal(content.childNodes.length, 4);
var hl = content.querySelector('hl.some');
assert.strictEqual(content.firstChild, hl.previousSibling);
assert.equal(hl.childNodes.length, 1);
assert.equal(hl.childNodes.length, 5);
assert.equal(content.querySelectorAll('span.tab').length, 2);
assert.equal(hl.textContent,
'a💢ti te, inquit, sumus aliquando otiosum, certe a udiam, ' +
'quid sit, quod Epicuru');
@@ -435,7 +449,7 @@ limitations under the License.
// After: na💢ti <hl class="foo">te, in</hl><hl class="some">quit, ...
element._applyRangedHighlight('some', 140, 12, 140, 21, 'left');
var hl = content.querySelector('hl.some');
assert.equal(hl.outerHTML, '<hl class="some">quit, sum</hl>');
assert.equal(hl.textContent, 'quit, sum');
assert.equal(
hl.previousSibling.outerHTML, '<hl class="foo">te, in</hl>');
});
@@ -445,7 +459,7 @@ limitations under the License.
// After: <hl class="foo">t</hl><hl="some">e, i</hl><hl class="foo">n..
element._applyRangedHighlight('some', 140, 7, 140, 12, 'left');
var hl = content.querySelector('hl.some');
assert.equal(hl.outerHTML, '<hl class="some">e, in</hl>');
assert.equal(hl.textContent, 'e, in');
assert.equal(hl.previousSibling.outerHTML, '<hl class="foo">t</hl>');
assert.equal(hl.nextSibling.outerHTML, '<hl class="foo">quit</hl>');
});
@@ -453,7 +467,7 @@ limitations under the License.
test('hl starts and ends in different hls', function() {
element._applyRangedHighlight('some', 140, 8, 140, 27, 'left');
var hl = content.querySelector('hl.some');
assert.equal(hl.outerHTML, '<hl class="some">, inquit, sumus ali</hl>');
assert.equal(hl.textContent, ', inquit, sumus ali');
assert.equal(hl.previousSibling.outerHTML, '<hl class="foo">te</hl>');
assert.equal(hl.nextSibling.outerHTML, '<hl class="bar">quando</hl>');
});
@@ -468,9 +482,7 @@ limitations under the License.
test('hl starting and ending in boundaries', function() {
element._applyRangedHighlight('some', 140, 6, 140, 33, 'left');
var hl = content.querySelector('hl.some');
assert.equal(
hl.outerHTML, '<hl class="some">te, inquit, sumus aliquando</hl>');
assert.notOk(content.querySelector('.foo'));
assert.equal(hl.textContent, 'te, inquit, sumus aliquando');
assert.notOk(content.querySelector('.bar'));
});
@@ -482,7 +494,7 @@ limitations under the License.
assert.equal(hl.outerHTML, '<hl class="some">a💢t</hl>');
});
test('growing hl left including another hl', function() {
test('growing hl right including another hl', function() {
element._applyRangedHighlight('some', 140, 1, 140, 4, 'left');
element._applyRangedHighlight('some', 140, 3, 140, 10, 'left');
assert.equal(content.querySelectorAll('hl.some').length, 1);
@@ -491,7 +503,7 @@ limitations under the License.
assert.equal(hl.nextSibling.outerHTML, '<hl class="foo">inquit</hl>');
});
test('growing hl right to start of line', function() {
test('growing hl left to start of line', function() {
element._applyRangedHighlight('some', 140, 2, 140, 5, 'left');
element._applyRangedHighlight('some', 140, 0, 140, 3, 'left');
assert.equal(content.querySelectorAll('hl.some').length, 1);
@@ -499,6 +511,14 @@ limitations under the License.
assert.equal(hl.outerHTML, '<hl class="some">na💢ti</hl>');
assert.strictEqual(content.firstChild, hl);
});
test('splitting hl containing a tab', function() {
element._applyRangedHighlight('some', 140, 63, 140, 72, 'left');
assert.equal(content.querySelector('hl.some').textContent, 'sit, quod');
element._applyRangedHighlight('another', 140, 66, 140, 81, 'left');
assert.equal(content.querySelector('hl.another').textContent,
', quod Epicurum');
});
});
test('_applyAllHighlights', function() {
@@ -644,6 +664,21 @@ limitations under the License.
});
test('multiline', function() {
var startContent = stubContent(119, 'right');
var endContent = stubContent(120, 'right');
emulateSelection(
startContent.firstChild, 10, endContent.lastChild, 7);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 119,
startChar: 10,
endLine: 120,
endChar: 34,
});
assert.equal(getActionSide(), 'right');
});
test('multiline grow end highlight over tabs', function() {
var startContent = stubContent(119, 'right');
var endContent = stubContent(120, 'right');
emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
@@ -693,7 +728,7 @@ limitations under the License.
test('multiple hl', function() {
var content = stubContent(140, 'left');
var hl = content.querySelectorAll('hl')[3];
var hl = content.querySelectorAll('hl')[4];
emulateSelection(content.firstChild, 2, hl.firstChild, 2);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
@@ -766,7 +801,7 @@ limitations under the License.
test('ends in context element', function() {
var contextControl = diff.querySelector('.contextControl');
var content = stubContent(141, 'left');
emulateSelection(content.firstChild, 7, contextControl, 0);
emulateSelection(content.firstChild, 2, contextControl, 0);
// TODO (viktard): Select nearest line.
assert.isFalse(element.isRangeSelected());
});
@@ -785,6 +820,35 @@ limitations under the License.
assert.equal(getActionSide(), 'right');
});
test('ends at a tab', function() {
var content = stubContent(140, 'left');
emulateSelection(
content.firstChild, 1, content.querySelector('span'), 0);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 140,
startChar: 1,
endLine: 140,
endChar: 51,
});
assert.equal(getActionSide(), 'left');
});
test('starts at a tab', function() {
var content = stubContent(140, 'left');
emulateSelection(
content.querySelectorAll('hl')[3], 0,
content.querySelectorAll('span')[1], 0);
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 140,
startChar: 51,
endLine: 140,
endChar: 68,
});
assert.equal(getActionSide(), 'left');
});
// TODO (viktard): Selection starts in line number.
// TODO (viktard): Empty lines in selection start.
// TODO (viktard): Empty lines in selection end.