diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html index 277f9b683b..315692a299 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html @@ -149,7 +149,11 @@ limitations under the License. var root = Polymer.dom(opt_root || this.diffElement); var sideSelector = !!opt_side ? ('.' + opt_side) : ''; var content = this.getContentByLine(lineNumber, opt_side, opt_root); - return content.querySelector('gr-diff-comment-thread'); + return this.getCommentThreadByContentEl(content); + }, + + getCommentThreadByContentEl: function(contentEl) { + return contentEl.querySelector('gr-diff-comment-thread'); }, getSideByLineEl: function(lineEl) { diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js index 828bb01abe..dcdf9452b5 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js @@ -14,6 +14,9 @@ (function() { 'use strict'; + // Astral code point as per https://mathiasbynens.be/notes/javascript-unicode + var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/; + Polymer({ is: 'gr-diff-highlight', @@ -44,13 +47,6 @@ return this._cachedDiffBuilder; }, - get diffElement() { - if (!this._diffElement) { - this._diffElement = Polymer.dom(this).querySelector('#diffTable'); - } - return this._diffElement; - }, - detached: function() { this.enabled = false; }, @@ -76,5 +72,307 @@ this._removeActionBox(); } }, + + _removeActionBox: function() { + var actionBox = this.$$('gr-selection-action-box'); + if (actionBox) { + Polymer.dom(this.root).removeChild(actionBox); + } + }, + + /** + * Traverse diff content from right to left, call callback for each node. + * Stops if callback returns true. + * + * @param {!Node} startNode + * @param {function(Node):boolean} callback + * @param {Object=} flags If flags.left is true, traverse left. + */ + _traverseContentSiblings: function(startNode, callback, opt_flags) { + var travelLeft = opt_flags && opt_flags.left; + var node = startNode; + while (node) { + if (node instanceof Element && node.tagName !== 'HL') { + break; + } + var nextNode = travelLeft ? node.previousSibling : node.nextSibling; + if (callback(node)) { + break; + } + node = nextNode; + } + }, + + /** + * Get length of a node. Traverses diff content siblings if required. + * + * @param {!Node} node + * @return {number} + */ + _getLength: function(node) { + if (node instanceof Element && node.classList.contains('content')) { + node = node.firstChild; + var length = 0; + while (node) { + // Only measure Text nodes and + if (node instanceof Text || node.tagName == 'HL') { + length += this._getLength(node); + } + node = node.nextSibling; + } + return length; + } else { + // DOM API for textConten.length is broken for Unicode: + // https://mathiasbynens.be/notes/javascript-unicode + return node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length; + } + }, + + /** + * Wraps node in hl tag with cssClass, replacing the node in DOM. + * + * @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); + return hl; + }, + + /** + * Node.prototype.splitText Unicode-valid alternative. + * + * @param {!Text} node + * @param {number} offset + * @return {!Text} Trailing Text Node. + */ + _splitText: function(node, offset) { + if (node.textContent.match(REGEX_ASTRAL_SYMBOL)) { + // DOM Api for splitText() is broken for Unicode: + // https://mathiasbynens.be/notes/javascript-unicode + // TODO (viktard): Polyfill Array.from for IE10. + var head = Array.from(node.textContent); + var tail = head.splice(offset); + var parent = node.parentElement; + var headNode = document.createTextNode(head.join('')); + parent.replaceChild(headNode, node); + var tailNode = document.createTextNode(tail.join('')); + parent.insertBefore(tailNode, headNode.nextSibling); + return tailNode; + } else { + return node.splitText(offset); + } + }, + + /** + * 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 {number} offset + * @param {string} cssClass + * @param {boolean=} opt_firstPart + */ + _splitAndWrapInHighlight: function(node, offset, cssClass, opt_firstPart) { + if (this._getLength(node) === offset || offset === 0) { + return this._wrapInHighlight(node, cssClass); + } else { + if (opt_firstPart) { + this._splitText(node, offset); + // Node points to first part of the Text, second one is sibling. + } else { + node = this._splitText(node, offset); + } + return this._wrapInHighlight(node, cssClass); + } + }, + + /** + * Creates hl tag with cssClass for starting side of range highlight. + * + * @param {!Element} startContent Range start diff content aka td.content. + * @param {!Element} endContent Range end diff content aka td.content. + * @param {number} startOffset Range start within start content. + * @param {number} endOffset Range end within end content. + * @param {string} cssClass + * @return {!Element} Range start node. + */ + _normalizeStart: function( + startContent, endContent, startOffset, endOffset, cssClass) { + var isOneLine = startContent === endContent; + var startNode = startContent.firstChild; + var length = endOffset - startOffset; + + if (!startNode) { + return startNode; + } + + // Skip nodes before startOffset. + while (startNode && + this._getLength(startNode) <= startOffset || + this._getLength(startNode) === 0) { + startOffset -= this._getLength(startNode); + startNode = startNode.nextSibling; + } + + // Split Text node. + 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); + 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, 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(); + } + } + } else { + startNode = null; + } + return startNode; + }, + + /** + * Creates hl tag with cssClass for ending side of range highlight. + * + * @param {!Element} startContent Range start diff content aka td.content. + * @param {!Element} endContent Range end diff content aka td.content. + * @param {number} startOffset Range start within start content. + * @param {number} endOffset Range end within end content. + * @param {string} cssClass + * @return {!Element} Range start node. + */ + _normalizeEnd: function( + startContent, endContent, startOffset, endOffset, cssClass) { + var endNode = endContent.firstChild; + + if (!endNode) { + return endNode; + } + + // Find the node where endOffset points at. + while (endNode && + this._getLength(endNode) < endOffset || + this._getLength(endNode) === 0) { + endOffset -= this._getLength(endNode); + endNode = endNode.nextSibling; + } + + if (endNode instanceof Text) { + endNode = + this._splitAndWrapInHighlight(endNode, endOffset, cssClass, true); + } else if (endNode.tagName == 'HL') { + if (!endNode.classList.contains(cssClass)) { + // Split text inside HL. + var hl = endNode; + endNode = this._splitAndWrapInHighlight( + endNode.firstChild, endOffset, cssClass, true); + endContent.insertBefore(endNode, hl); + if (hl.textContent.length === 0) { + hl.remove(); + } + } + } else { + endNode = null; + } + return endNode; + }, + + /** + * Applies highlight to first and last lines in range. + * + * @param {!Element} startContent Range start diff content aka td.content. + * @param {!Element} endContent Range end diff content aka td.content. + * @param {number} startOffset Range start within start content. + * @param {number} endOffset Range end within end content. + * @param {string} cssClass + */ + _highlightSides: function( + startContent, endContent, startOffset, endOffset, cssClass) { + var isOneLine = startContent === endContent; + var startNode = this._normalizeStart( + startContent, endContent, startOffset, endOffset, cssClass); + var endNode = this._normalizeEnd( + startContent, endContent, startOffset, endOffset, cssClass); + + // 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(); + return node == endNode; + }); + } + + if (!isOneLine && endNode) { + // 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}); + } + }, + + /** + * @param {string} cssClass + * @param {number} startLine Range start code line number. + * @param {number} startCol Range start column number. + * @param {number} endCol Range end column number. + * @param {number} endOffset Range end within end content. + * @param {string=} opt_side Side selector (right or left). + */ + _applyRangedHighlight: function( + cssClass, startLine, startCol, endLine, endCol, opt_side) { + var side = opt_side; + var startEl = this.diffBuilder.getContentByLine(startLine, opt_side); + var endEl = this.diffBuilder.getContentByLine(endLine, opt_side); + this._highlightSides(startEl, endEl, startCol, endCol, cssClass); + if (endLine - startLine > 1) { + // There is at least one line in between. + var contents = this.diffBuilder.getContentsByLineRange( + startLine + 1, endLine - 1, opt_side); + // Wrap contents in highlight. + contents.forEach(function(content) { + if (content.textContent.length === 0) { + return; + } + var lineEl = this.diffBuilder.getLineElByChild(content); + var line = lineEl.getAttribute('data-value'); + var threadEl = + this.diffBuilder.getCommentThreadByContentEl(content); + if (threadEl) { + threadEl.remove(); + } + var text = document.createTextNode(content.textContent); + while (content.firstChild) { + content.removeChild(content.firstChild); + } + content.appendChild(text); + if (threadEl) { + content.appendChild(threadEl); + } + this._wrapInHighlight(text, cssClass); + }, this); + } + }, }); })(); diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html index 1a7a6ea0be..fd0ea27453 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html @@ -26,18 +26,72 @@ limitations under the License. + + + +