hterm: double-click expands selection if inside URL.

Change-Id: I80d274335c3e34b78f9ff065e4323863049235e5
BUG=None
TEST=None
Reviewed-on: https://gerrit.chromium.org/gerrit/62865
Commit-Queue: Robert Ginda <rginda@chromium.org>
Reviewed-by: Robert Ginda <rginda@chromium.org>
Tested-by: Robert Ginda <rginda@chromium.org>
diff --git a/hterm/js/hterm_screen.js b/hterm/js/hterm_screen.js
index 4c5f459..1e5ab47 100644
--- a/hterm/js/hterm_screen.js
+++ b/hterm/js/hterm_screen.js
@@ -625,3 +625,227 @@
 
   return rv;
 };
+
+/**
+ * Finds first X-ROW of a line containing specified X-ROW.
+ * Used to support line overflow.
+ *
+ * @param {Node} row X-ROW to begin search for first row of line.
+ * @return {Node} The X-ROW that is at the beginning of the line.
+ **/
+hterm.Screen.prototype.getLineStartRow_ = function(row) {
+  while (row.previousSibling &&
+         row.previousSibling.hasAttribute('line-overflow')) {
+    row = row.previousSibling;
+  }
+  return row;
+};
+
+/**
+ * Gets text of a line beginning with row.
+ * Supports line overflow.
+ *
+ * @param {Node} row First X-ROW of line.
+ * @return {string} Text content of line.
+ **/
+hterm.Screen.prototype.getLineText_ = function(row) {
+  var rowText = "";
+  while (row) {
+    rowText += row.textContent;
+    if (row.hasAttribute('line-overflow')) {
+      row = row.nextSibling;
+    } else {
+      break;
+    }
+  }
+  return rowText;
+};
+
+/**
+ * Returns X-ROW that is ancestor of the node.
+ *
+ * @param {Node} node Node to get X-ROW ancestor for.
+ * @return {Node} X-ROW ancestor of node, or null if not found.
+ **/
+hterm.Screen.prototype.getXRowAncestor_ = function(node) {
+  while (node) {
+    if (node.nodeName === 'X-ROW')
+      break;
+    node = node.parentNode;
+  }
+  return node;
+};
+
+/**
+ * Returns position within line of character at offset within node.
+ * Supports line overflow.
+ *
+ * @param {Node} row X-ROW at beginning of line.
+ * @param {Node} node Node to get position of.
+ * @param {integer} offset Offset into node.
+ *
+ * @return {integer} Position within line of character at offset within node.
+ **/
+hterm.Screen.prototype.getPositionWithOverflow_ = function(row, node, offset) {
+  if (!node)
+    return -1;
+  var ancestorRow = this.getXRowAncestor_(node);
+  if (!ancestorRow)
+    return -1;
+  var position = 0;
+  while (ancestorRow != row) {
+    position += row.textContent.length;
+    if (row.hasAttribute('line-overflow') && row.nextSibling) {
+      row = row.nextSibling;
+    } else {
+      return -1;
+    }
+  }
+  return position + this.getPositionWithinRow_(row, node, offset);
+};
+
+/**
+ * Returns position within row of character at offset within node.
+ * Does not support line overflow.
+ *
+ * @param {Node} row X-ROW to get position within.
+ * @param {Node} node Node to get position for.
+ * @param {integer} offset Offset within node to get position for.
+ * @return {integer} Position within row of character at offset within node.
+ **/
+hterm.Screen.prototype.getPositionWithinRow_ = function(row, node, offset) {
+  if (node.parentNode != row) {
+    return this.getPositionWithinRow_(node.parentNode, node, offset) +
+           this.getPositionWithinRow_(row, node.parentNode, 0);
+  }
+  var position = 0;
+  for (var i = 0; i < row.childNodes.length; i++) {
+    var currentNode = row.childNodes[i];
+    if (currentNode == node)
+      return position + offset;
+    position += currentNode.textContent.length;
+  }
+  return -1;
+};
+
+/**
+ * Returns the node and offset corresponding to position within line.
+ * Supports line overflow.
+ *
+ * @param {Node} row X-ROW at beginning of line.
+ * @param {integer} position Position within line to retrieve node and offset.
+ * @return {Array} Two element array containing node and offset respectively.
+ **/
+hterm.Screen.prototype.getNodeAndOffsetWithOverflow_ = function(row, position) {
+  while (row && position > row.textContent.length) {
+    if (row.hasAttribute('line-overflow') && row.nextSibling) {
+      position -= row.textContent.length;
+      row = row.nextSibling;
+    } else {
+      return -1;
+    }
+  }
+  return this.getNodeAndOffsetWithinRow_(row, position);
+};
+
+/**
+ * Returns the node and offset corresponding to position within row.
+ * Does not support line overflow.
+ *
+ * @param {Node} row X-ROW to get position within.
+ * @param {integer} position Position within row to retrieve node and offset.
+ * @return {Array} Two element array containing node and offset respectively.
+ **/
+hterm.Screen.prototype.getNodeAndOffsetWithinRow_ = function(row, position) {
+  for (var i = 0; i < row.childNodes.length; i++) {
+    var node = row.childNodes[i];
+    if (position <= node.textContent.length) {
+      if (node.nodeName === 'SPAN') {
+        /** Drill down to node contained by SPAN. **/
+        return this.getNodeAndOffsetWithinRow_(node, position);
+      } else {
+        return [node, position];
+      }
+    }
+    position -= node.textContent.length;
+  }
+  return null;
+};
+
+/**
+ * Returns the node and offset corresponding to position within line.
+ * Supports line overflow.
+ *
+ * @param {Node} row X-ROW at beginning of line.
+ * @param {integer} start Start position of range within line.
+ * @param {integer} end End position of range within line.
+ * @param {Range} range Range to modify.
+ **/
+hterm.Screen.prototype.setRange_ = function(row, start, end, range) {
+  var startNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, start);
+  if (startNodeAndOffset == null)
+    return;
+  var endNodeAndOffset = this.getNodeAndOffsetWithOverflow_(row, end);
+  if (endNodeAndOffset == null)
+    return;
+  range.setStart(startNodeAndOffset[0], startNodeAndOffset[1]);
+  range.setEnd(endNodeAndOffset[0], endNodeAndOffset[1]);
+};
+
+/**
+ * Expands selection to surround URLs.
+ *
+ * Uses this regular expression to expand the selection:
+ * [^\s\[\](){}<>"'\^!@#$%&*,.;:~`]
+ * [^\s\[\](){}<>"'\^]*
+ * [^\s\[\](){}<>"'\^!@#$%&*,.;:~`]
+ *
+ * @param {Selection} selection Selection to expand.
+ **/
+hterm.Screen.prototype.expandSelection = function(selection) {
+  if (!selection)
+    return;
+
+  var range = selection.getRangeAt(0);
+  if (!range || range.toString().match(/\s/))
+    return;
+
+  var row = this.getLineStartRow_(this.getXRowAncestor_(range.startContainer));
+  if (!row)
+    return;
+
+  var startPosition = this.getPositionWithOverflow_(row,
+                                                    range.startContainer,
+                                                    range.startOffset);
+  if (startPosition == -1)
+    return;
+  var endPosition = this.getPositionWithOverflow_(row,
+                                                  range.endContainer,
+                                                  range.endOffset);
+  if (endPosition == -1)
+    return;
+
+  var outsideMatch = '[^\\s\\[\\](){}<>"\'\\^!@#$%&*,.;:~`]';
+  var insideMatch = '[^\\s\\[\\](){}<>"\'\\^]*';
+
+  //Move start to the left.
+  var rowText = this.getLineText_(row);
+  var lineUpToRange = rowText.substring(0, endPosition);
+  var leftRegularExpression = new RegExp(outsideMatch + insideMatch + "$");
+  var expandedStart = lineUpToRange.search(leftRegularExpression);
+  if (expandedStart == -1 || expandedStart > startPosition)
+    return;
+
+  //Move end to the right.
+  var lineFromRange = rowText.substring(startPosition, rowText.length);
+  var rightRegularExpression = new RegExp("^" + insideMatch + outsideMatch);
+  var found = lineFromRange.match(rightRegularExpression);
+  if (!found)
+    return;
+  var expandedEnd = startPosition + found[0].length;
+  if (expandedEnd == -1 || expandedEnd < endPosition)
+    return;
+
+  this.setRange_(row, expandedStart, expandedEnd, range);
+  selection.addRange(range);
+};
diff --git a/hterm/js/hterm_terminal.js b/hterm/js/hterm_terminal.js
index 2443752..01be183 100644
--- a/hterm/js/hterm_terminal.js
+++ b/hterm/js/hterm_terminal.js
@@ -2414,6 +2414,12 @@
 
   e.processedByTerminalHandler_ = true;
 
+  if (e.type == 'dblclick') {
+    this.screen_.expandSelection(this.document_.getSelection());
+    hterm.copySelectionToClipboard(this.document_);
+    return;
+  }
+
   if (e.type == 'mousedown' && e.which == this.mousePasteButton) {
     this.paste();
     return;