namespace-browser: Service Signature Compatibility Update

- Adapated Signature now reveals more information.
  + argument type information (inArgs only, not outArgs)
  + doc strings (shown in core-tooltip) for methods and interfaces
- Polymer tweaking
  + label attribute was removed on paper-button
  + core-tooltip's internal overlay has .core-tooltip as its class
- Mercury tweaking
  + span was added around various text nodes.
    For some reason, mercury is not diffing them properly.

Screenshots:
* https://screenshot.googleplex.com/UEPot1EF9t
* https://screenshot.googleplex.com/ft28NTLWZX

Change-Id: I92bc8c40830fd9842d4e1ba2012200eba1ff6ab3
diff --git a/src/components/browse/index.css b/src/components/browse/index.css
index fe8f78a..bc1afd6 100644
--- a/src/components/browse/index.css
+++ b/src/components/browse/index.css
@@ -107,17 +107,11 @@
   color: var(--color-text-primary-invert);
 }
 
-.tooltip:hover::shadow .polymer-tooltip,
-.tooltip:focus::shadow .polymer-tooltip {
-  opacity: 1;
-  transform: translate3d(0, 0, 0);
-}
-
-.tooltip::shadow .polymer-tooltip {
-  opacity: 0;
-  transition: all 300ms cubic-bezier(0,1.92,.99,1.07);
-  transition-delay: 0.5s;
-  transform: translate3d(0, -10px, 0);
+.tooltip::shadow .core-tooltip {
+  width: 36em;
+  line-height: 0.9em; /* overrides Polymer's 6px line-height */
+  white-space: pre-wrap;
+  font-size: var(--size-font-xsmall);
 }
 
 .breadcrumbs {
@@ -185,6 +179,7 @@
 
 .search-box core-tooltip {
   width: 100%;
+  font-size: var(--size-font-xsmall);
 }
 
 .search-box .icon, .namespace-box .icon {
@@ -194,7 +189,7 @@
 }
 
 .search-box .input {
-  font-size: var(--size-font-xsmall)
+  font-size: var(--size-font-xsmall);
 }
 
 /* Make space for the clear search icon */
diff --git a/src/components/browse/item-details/index.css b/src/components/browse/item-details/index.css
index ec35868..2ff892f 100644
--- a/src/components/browse/item-details/index.css
+++ b/src/components/browse/item-details/index.css
@@ -18,6 +18,10 @@
   overflow: hidden;
 }
 
+.tooltip.method-tooltip::shadow .core-tooltip {
+  width: 24em;
+}
+
 .method-input .label {
   align-items: center;
   overflow: hidden;
@@ -78,6 +82,10 @@
   color: var(--color-error);
 }
 
+core-icon.info {
+  color: var(--color-dark);
+}
+
 /* Chrome, Safari, Opera */
 @-webkit-keyframes myfirst {
   0%   {background:cyan; width:0%;}
diff --git a/src/components/browse/item-details/index.js b/src/components/browse/item-details/index.js
index 18d5d46..b0f83e9 100644
--- a/src/components/browse/item-details/index.js
+++ b/src/components/browse/item-details/index.js
@@ -127,11 +127,41 @@
   } else {
     typeName = 'Intermediary Name';
   }
+
   var displayItems = [
     renderFieldItem('Name', (item.objectName || '<root>')),
     renderFieldItem('Type', typeName, typeDescription)
   ];
 
+  if (item.isServer) {
+    // Display each service description and show it.
+    var serviceDescs = [];
+    var descs = state.item.serverInfo.signature.pkgNameDescriptions;
+    Object.keys(descs).forEach(function(pkgName) {
+      var desc = descs[pkgName];
+
+      // Use an info icon whose tooltip reveals the description.
+      serviceDescs.push(h('div', [
+        h('core-tooltip.tooltip', {
+          'label': desc || '<no description>',
+          'position': 'right'
+        }, h('core-icon.icon.info', {
+          'icon': new AttributeHook('info')
+        })),
+        h('span.margin-left-xxsmall', pkgName)
+      ]));
+    });
+
+    if (serviceDescs.length > 0) {
+      displayItems.push(
+        renderFieldItem('Interfaces', h('div', {
+          'vertical': new AttributeHook(true),
+          'layout': new AttributeHook(true)
+        }, serviceDescs))
+      );
+    }
+  }
+
   return [
     h('div', displayItems)
   ];
@@ -155,7 +185,7 @@
 function renderMethodSignatures(state, events) {
   var sig = state.item.serverInfo.signature;
   if (!sig) {
-    return h('div', 'No method signature');
+    return h('div', h('span', 'No method signature'));
   }
 
   var methods = [];
@@ -184,7 +214,7 @@
 function renderMethodOutput(state) {
   var outputs = state.methodOutputs[state.item.objectName];
   if (outputs === undefined) {
-    return h('div.method-output', 'No method output');
+    return h('div.method-output', h('span', 'No method output'));
   }
   var outputRows = [h('tr', [
     h('th', '#'),
diff --git a/src/components/browse/item-details/method-end.js b/src/components/browse/item-details/method-end.js
index fd75462..2a25f1f 100644
--- a/src/components/browse/item-details/method-end.js
+++ b/src/components/browse/item-details/method-end.js
@@ -1,3 +1,4 @@
+var h = require('mercury').h;
 var smartService = require('../../../services/smart/service');
 var log = require('../../../lib/log')(
   'components:browse:item-details:method-end');
@@ -31,9 +32,9 @@
 
   // Do not process results we expect to be empty.
   // TODO(alexfandrianto): Streaming results are ignored with this logic.
-  var expectedOutArgs = sig.get(method).numOutArgs;
+  var expectedOutArgs = sig.get(method).outArgs.length;
   if (expectedOutArgs === 1) { // Error is the only possible out argument.
-    replaceResult(state, data.runID, '<ok>');
+    replaceResult(state, data.runID, h('span', '<ok>'));
     return;
   }
 
diff --git a/src/components/browse/item-details/method-form/index.js b/src/components/browse/item-details/method-form/index.js
index 7b75ef7..c459f14 100644
--- a/src/components/browse/item-details/method-form/index.js
+++ b/src/components/browse/item-details/method-form/index.js
@@ -148,7 +148,7 @@
     param.inArgs.map(function(inArg, i) {
       return smartService.predict(
         'learner-method-input',
-        _.assign({argName: inArg}, input)
+        _.assign({argName: inArg.name}, input)
       ).then(function(inputSuggestion) {
         state.inputSuggestions.put(i, inputSuggestion);
       });
@@ -287,7 +287,7 @@
 
   // Return immediately if we don't need arguments or haven't expanded.
   if (state.args.length === 0 || !state.expanded) {
-    return h('div.method-input', methodNameHeader);
+    return makeMethodTooltip(state, h('div.method-input', methodNameHeader));
   }
 
   // Render the stars first, and if there's extra room, the recommendations.
@@ -305,7 +305,18 @@
   var runButton = renderRPCRunButton(state, events);
 
   var footer = h('div.method-input-expanded', [argForm, starButton, runButton]);
-  return h('div.method-input', [methodNameHeader, recs, footer]);
+  return makeMethodTooltip(state,
+    h('div.method-input', [methodNameHeader, recs, footer]));
+}
+
+/*
+ * Wrap the method form with a tooltip.
+ */
+function makeMethodTooltip(state, child) {
+  return h('core-tooltip.tooltip.method-tooltip', {
+    'label': state.signature.get(state.methodName).doc || '<no description>',
+    'position': 'top'
+  }, child);
 }
 
 /*
@@ -316,9 +327,7 @@
     return renderInvocation(state, events);
   }
   var labelText = getMethodSignature(state);
-  var label = h('div.label', {
-    'title': labelText
-  }, labelText);
+  var label = makeMethodLabel(labelText);
 
   var expand = h('a.drill', {
     'href': 'javascript:;',
@@ -342,7 +351,12 @@
   var param = state.signature.get(methodName);
   var text = methodName + '(';
   for (var i = 0; i < param.inArgs.length; i++) {
-    var arg = args !== undefined ? args[i] : param.inArgs[i];
+    var arg = '';
+    if (args !== undefined) {
+      arg = args[i];
+    } else {
+      arg = param.inArgs[i].name + ' ' + param.inArgs[i].type.toString();
+    }
     if (i > 0) {
       text += ',';
     }
@@ -394,9 +408,7 @@
   var noArgs = argsStr === undefined;
   var args = noArgs ? [] : JSON.parse(argsStr);
   var labelText = getMethodSignature(state, args);
-  var label = h('div.label', {
-    'title': labelText
-  }, labelText);
+  var label = makeMethodLabel(labelText);
 
   var runButton = h('a.drill', {
     'href': 'javascript:;',
@@ -422,12 +434,21 @@
 }
 
 /*
+ * Render a method label using labelText.
+ */
+function makeMethodLabel(labelText) {
+  return h('div.label', {
+    'title': labelText
+  }, labelText);
+}
+
+/*
  * Draws a single method argument input using the paper-autocomplete element.
  * Includes a placeholder and suggestions from the internal state.
  */
 function renderMethodInput(state, index) {
   var methodName = state.methodName;
-  var argName = state.signature.get(methodName).inArgs[index];
+  var argName = state.signature.get(methodName).inArgs[index].name;
   var inputSuggestions = state.inputSuggestions[index];
   var args = state.args;
 
@@ -466,10 +487,12 @@
       'raised': new AttributeHook('true'),
       'ev-click': mercury.event(events.starAction, {
         star: true
-      }),
-      'label': 'STAR'
+      })
     },
-    renderStarIcon(false)
+    [
+      renderStarIcon(false),
+      h('span', 'Star')
+    ]
   );
   return starButton;
 }
@@ -483,10 +506,12 @@
     {
       'href': 'javascript:;',
       'raised': new AttributeHook('true'),
-      'ev-click': getRunEvent(state, events, state.args),
-      'label': 'RUN'
+      'ev-click': getRunEvent(state, events, state.args)
     },
-    renderPlayIcon()
+    [
+      renderPlayIcon(),
+      h('span', 'Run')
+    ]
   );
   return runButton;
 }
diff --git a/src/components/browse/item-details/plugins/default.js b/src/components/browse/item-details/plugins/default.js
index 0662125..b6df1f9 100644
--- a/src/components/browse/item-details/plugins/default.js
+++ b/src/components/browse/item-details/plugins/default.js
@@ -1,3 +1,5 @@
+var h = require('mercury').h;
+
 module.exports = {
   'shouldFormat': shouldFormat,
   'format': format
@@ -14,5 +16,5 @@
  * By default, the input is returned as prettified JSON.
  */
 function format(input) {
-  return JSON.stringify(input, null, 2);
+  return h('span', JSON.stringify(input, null, 2));
 }
\ No newline at end of file
diff --git a/src/components/browse/item-details/plugins/empty.js b/src/components/browse/item-details/plugins/empty.js
index 38d8e75..dceccea 100644
--- a/src/components/browse/item-details/plugins/empty.js
+++ b/src/components/browse/item-details/plugins/empty.js
@@ -1,3 +1,5 @@
+var h = require('mercury').h;
+
 module.exports = {
   'shouldFormat': shouldFormat,
   'format': format
@@ -15,5 +17,5 @@
  * Indicate that nothing was there.
  */
 function format(input) {
-  return '<no data>';
+  return h('span', '<no data>');
 }
\ No newline at end of file
diff --git a/src/components/browse/item-details/plugins/error.js b/src/components/browse/item-details/plugins/error.js
index 18d8ca9..73f3739 100644
--- a/src/components/browse/item-details/plugins/error.js
+++ b/src/components/browse/item-details/plugins/error.js
@@ -17,7 +17,7 @@
  * Print the error with a dangerous-looking icon.
  */
 function format(input) {
-  return h('span', [
+  return h('div', [
     h('core-icon.error', {
       'icon': new AttributeHook('error')
     }),
diff --git a/src/components/browse/item-details/plugins/histogram.js b/src/components/browse/item-details/plugins/histogram.js
index 15bb923..e908eb6 100644
--- a/src/components/browse/item-details/plugins/histogram.js
+++ b/src/components/browse/item-details/plugins/histogram.js
@@ -1,3 +1,4 @@
+var h = require('mercury').h;
 var histogram = require('bars');
 
 module.exports = {
@@ -23,5 +24,5 @@
   input.buckets.forEach(function(obj) {
     histData[obj.lowBound] = obj.count;
   });
-  return histogram(histData, { bar: '*', width: 20 });
+  return h('span', histogram(histData, { bar: '*', width: 20 }));
 }
\ No newline at end of file
diff --git a/src/components/common-style/defaults.css b/src/components/common-style/defaults.css
index 5817a5a..33efad7 100644
--- a/src/components/common-style/defaults.css
+++ b/src/components/common-style/defaults.css
@@ -64,9 +64,24 @@
 paper-button {
   background-color: var(--color-grey-light);
   margin: var(--size-space-xsmall);
-  max-height: 3em;
+}
+
+/*
+ * The paper-button looks better with reduced padding.
+ * Polymer defaults to 0.70em for padding-top and padding-bottom.
+ */
+paper-button::shadow .button-content {
+  padding-top: 0.35em;
+  padding-bottom: 0.35em;
 }
 
 paper-button > core-icon {
   margin-right: var(--size-space-xsmall);
+}
+
+/*
+ * Applies a tiny margin to the left of the element.
+ */
+.margin-left-xxsmall {
+  margin-left: var(--size-space-xxsmall);
 }
\ No newline at end of file
diff --git a/src/components/common-style/sizes.css b/src/components/common-style/sizes.css
index aa9f600..e8d37d2 100644
--- a/src/components/common-style/sizes.css
+++ b/src/components/common-style/sizes.css
@@ -2,7 +2,7 @@
 :root {
 
   /* font-size */
-  --size-font-xsmall: 0.65em;
+  --size-font-xsmall: 0.7em;
   --size-font-small: 0.9em;
   --size-font-normal: 1em;
   --size-font-large: 1.1em;
diff --git a/src/services/namespace/service.js b/src/services/namespace/service.js
index 464fdec..df8e74f 100644
--- a/src/services/namespace/service.js
+++ b/src/services/namespace/service.js
@@ -254,11 +254,11 @@
  * a service without containing unnecessary information.
  * TODO(alexfandrianto): This heuristic comes close, but it does not properly
  * distinguish services from each other.
- * Once available, add type info, streaming info, interface name, etc.
+ * The adapted signature now has type info, streaming info, interface name, etc.
  */
-function hashSignature(signature) {
-  var cp = adaptSignature([]);
-  signature.forEach(function(method, methodName) {
+function hashSignature(adaptedSignature) {
+  var cp = [];
+  adaptedSignature.forEach(function(method, methodName) {
     cp[methodName] = method.inArgs.length;
   });
   return jsonStableStringify(cp);
diff --git a/src/services/namespace/signature-adapter.js b/src/services/namespace/signature-adapter.js
index d1f364f..e99f351 100644
--- a/src/services/namespace/signature-adapter.js
+++ b/src/services/namespace/signature-adapter.js
@@ -2,15 +2,18 @@
 
 module.exports = adapt;
 /*
- * Adapts from IPC service Signatures to a custom signature struct specific
- * to this application.
- * TODO(aghasssemi) Consider separate signature instead of merging?
+ * Adapts from IPC service Signatures to a custom signature Map-specific
+ * to this application. Service methods, pkgName, and descriptions are added.
+ * TODO(aghasssemi): Consider separate signature instead of merging?
  */
 function adapt(signatures) {
   var adaptedSig = new Map();
-
+  adaptedSig.pkgNameDescriptions = [];
   signatures.forEach(function(sig) {
-    sig.methods.forEach( function(method) {
+    if (sig.name) {
+      adaptedSig.pkgNameDescriptions[sig.pkgPath + '.' + sig.name] = sig.doc;
+    }
+    sig.methods.forEach(function(method) {
       var key = vom.MiscUtil.uncapitalize(method.name);
       adaptedSig.set(key, method);
     });