Update the URL hash with the selected diff line

Sets the URL hash to the address of the cursor when a user selects a
diff line (either by clicking the line number or using the 'c' hotkey).
If the cursor is on a line of the revision, the address is the line
number. If the cursor is on a line of the base patch, the address is the
letter 'b' followed by the line number. Otherwise the address is the
empty string.

Bug: Issue 4206
Change-Id: Ic85da197eb6264ea1111cc34e781dccbc24d4d40
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index f3d00a7..a730a68 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -115,6 +115,14 @@
       this._fixSide();
     },
 
+    moveToLineNumber: function(number, side) {
+      var row = this._findRowByNumber(number, side);
+      if (row) {
+        this.side = side;
+        this.$.cursorManager.setCursor(row);
+      }
+    },
+
     /**
      * Get the line number element targeted by the cursor row and side.
      * @return {DOMElement}
@@ -159,6 +167,34 @@
       }
     },
 
+    /**
+     * Get a short address for the location of the cursor. Such as '123' for
+     * line 123 of the revision, or 'b321' for line 321 of the base patch.
+     * Returns an empty string if an address is not available.
+     * @return {String}
+     */
+    getAddress: function() {
+      if (!this.diffRow) { return ''; }
+
+      // Get the line-number cell targeted by the cursor. If the mode is unified
+      // then prefer the revision cell if available.
+      var cell;
+      if (this._getViewMode() === DiffViewMode.UNIFIED) {
+        cell = this.diffRow.querySelector('.lineNum.right');
+        if (!cell) {
+          cell = this.diffRow.querySelector('.lineNum.left');
+        }
+      } else {
+        cell = this.diffRow.querySelector('.lineNum.' + this.side);
+      }
+      if (!cell) { return ''; }
+
+      var number = cell.getAttribute('data-value');
+      if (!number || number === 'FILE') { return ''; }
+
+      return (cell.matches('.left') ? 'b' : '') + number;
+    },
+
     _getViewMode: function() {
       if (!this.diffRow) {
         return null;
@@ -284,5 +320,16 @@
         }
       }
     },
+
+    _findRowByNumber: function(targetNumber, side) {
+      var stops = this.$.cursorManager.stops;
+      var selector;
+      for (var i = 0; i < stops.length; i++) {
+        selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]';
+        if (stops[i].querySelector(selector)) {
+          return stops[i];
+        }
+      }
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index 4dbabf7..e3d7e46 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -181,5 +181,39 @@
       assert.equal(currentIndex, previousIndex + 1);
       assert.equal(cursorElement.side, 'left');
     });
+
+    test('getAddress', function() {
+      // It should initialize to the first chunk: line 5 of the revision.
+      assert.equal(cursorElement.getAddress(), '5');
+
+      // Revision line 4 is up.
+      cursorElement.moveUp();
+      assert.equal(cursorElement.getAddress(), '4');
+
+      // Base line 4 is left.
+      cursorElement.moveLeft();
+      assert.equal(cursorElement.getAddress(), 'b4');
+
+      // Moving to the next chunk takes it back to the start.
+      cursorElement.moveToNextChunk();
+      assert.equal(cursorElement.getAddress(), '5');
+
+      // The following chunk is a removal starting on line 10 of the base.
+      cursorElement.moveToNextChunk();
+      assert.equal(cursorElement.getAddress(), 'b10');
+
+      // Should be an empty string if there is no selection.
+      cursorElement.$.cursorManager.unsetCursor();
+      assert.equal(cursorElement.getAddress(), '');
+    });
+
+    test('_findRowByNumber', function() {
+      // Get the first ab row after the first chunk.
+      var row = Polymer.dom(diffElement.root).querySelectorAll('tr')[8];
+
+      // It should be line 8 on the right, but line 5 on the left.
+      assert.equal(cursorElement._findRowByNumber(8, 'right'), row);
+      assert.equal(cursorElement._findRowByNumber(5, 'left'), row);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 943b24b..0e9db86 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -210,7 +210,7 @@
           has-ranged-comments="[[_localPrefs.ranged_comments]]"
           project-config="[[_projectConfig]]"
           view-mode="[[_diffMode]]"
-          on-render="_handleDiffRender">
+          on-line-selected="_onLineSelected">
       </gr-diff>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index e3f0c0c..afc51cd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -457,5 +457,10 @@
     _computeModeSelectHidden: function() {
       return this._isImageDiff;
     },
+
+    _onLineSelected: function(e, detail) {
+      this.$.cursor.moveToLineNumber(detail.number, detail.side);
+      history.pushState(null, null, '#' + this.$.cursor.getAddress());
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index cf9e6d4..7c64908 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -27,6 +27,11 @@
   Polymer({
     is: 'gr-diff',
 
+    /**
+     * Fired when the user selects a line.
+     * @event line-selected
+     */
+
     properties: {
       changeNum: String,
       patchRange: Object,
@@ -118,6 +123,7 @@
     },
 
     addDraftAtLine: function(el) {
+      this._selectLine(el);
       this._getLoggedIn().then(function(loggedIn) {
         if (!loggedIn) { return; }
 
@@ -198,6 +204,13 @@
       }
     },
 
+    _selectLine: function(el) {
+      this.fire('line-selected', {
+        side: el.classList.contains('left') ? DiffSide.LEFT : DiffSide.RIGHT,
+        number: el.getAttribute('data-value'),
+      });
+    },
+
     _handleCreateComment: function(e) {
       var range = e.detail.range;
       var diffSide = e.detail.side;