Merge "Fix split text issue"
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 bfe103b..cc3846c 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
@@ -94,6 +94,43 @@
       }
     },
 
+    _getContentTextParent: function(target) {
+      var element = target;
+      if (element.nodeName === '#text') {
+        element = element.parentElement;
+      }
+      while (!element.classList.contains('contentText')) {
+        if (element.parentElement === null) {
+          return target;
+        }
+        element = element.parentElement;
+      }
+      return element;
+    },
+
+    /**
+     * Remap DOM range to whole lines of a diff if necessary. If the start or
+     * end containers are DOM elements that are singular pieces of syntax
+     * highlighting, the containers are remapped to the .contentText divs that
+     * contain the entire line of code.
+     *
+     * @param  {Object} range - the standard DOM selector range.
+     * @return {Object} A modified version of the range that correctly accounts
+     *     for syntax highlighting.
+     */
+    _normalizeRange: function(range) {
+      var startContainer = this._getContentTextParent(range.startContainer);
+      var startOffset = range.startOffset + this._getTextOffset(startContainer,
+          range.startContainer);
+      var endContainer = this._getContentTextParent(range.endContainer);
+      var endOffset = range.endOffset + this._getTextOffset(endContainer,
+          range.endContainer);
+      return {
+        start: this._normalizeSelectionSide(startContainer, startOffset),
+        end: this._normalizeSelectionSide(endContainer, endOffset),
+      };
+    },
+
     /**
      * Convert DOM Range selection to concrete numbers (line, column, side).
      * Moves range end if it's not inside td.content.
@@ -160,13 +197,12 @@
       if (range.collapsed) {
         return;
       }
-      var start =
-          this._normalizeSelectionSide(range.startContainer, range.startOffset);
+      var normalizedRange = this._normalizeRange(range);
+      var start = normalizedRange.start;
       if (!start) {
         return;
       }
-      var end =
-          this._normalizeSelectionSide(range.endContainer, range.endOffset);
+      var end = normalizedRange.end;
       if (!end) {
         return;
       }
@@ -270,5 +306,36 @@
         return GrAnnotation.getLength(node);
       }
     },
+
+    /**
+     * Gets the character offset of the child within the parent.
+     * Performs a synchronous in-order traversal from top to bottom of the node
+     * element, counting the length of the syntax until child is found.
+     *
+     * @param {!Element} The root DOM element to be searched through.
+     * @param {!Element} The child element being searched for.
+     * @return {number}
+     */
+    _getTextOffset: function(node, child) {
+      var count = 0;
+      var stack = [node];
+      while (stack.length) {
+        var n = stack.pop();
+        if (n === child) {
+          break;
+        }
+        if (n.childNodes && n.childNodes.length !== 0) {
+          var arr = [];
+          n.childNodes.forEach(function(_child) {
+            arr.push(_child);
+          });
+          arr.reverse();
+          stack = stack.concat(arr);
+        } else {
+          count += this._getLength(n);
+        }
+      }
+      return count;
+    },
   });
 })();
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 5f84e4f..2612f9a 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
@@ -487,6 +487,27 @@
         assert.equal(getActionSide(), 'left');
       });
 
+      test('properly accounts for syntax highlighting', function() {
+        var content = stubContent(140, 'left');
+        var spy = sinon.spy(element, '_normalizeRange');
+        emulateSelection(
+            content.querySelectorAll('hl')[3], 0,
+            content.querySelectorAll('span')[1], 0);
+        var spyCall = spy.getCall(0);
+        var range = window.getSelection().getRangeAt(0);
+        assert.notDeepEqual(spyCall.returnValue, range);
+      });
+
+      test('_getTextOffset computes text offset', function() {
+        var content = stubContent(140, 'left');
+        var child = content.lastChild.lastChild;
+        var result = element._getTextOffset(content, child);
+        assert.equal(result, 73);
+        content = stubContent(146, 'right');
+        child = content.lastChild;
+        result = element._getTextOffset(content, child);
+        assert.equal(result, 0);
+      });
       // TODO (viktard): Selection starts in line number.
       // TODO (viktard): Empty lines in selection start.
       // TODO (viktard): Empty lines in selection end.