Editor for accounts/groups list.

To implement some new UI feature, we need a way to specify list
of accounts and/or groups. Similar solutions already exists for 'change'
view, but it can't be reused as-is in other places.
In this commit the existing solution was reworked to use the same
editor in different places.
Editor was splitted to 2 parts - editor itself and suggestion provider.

Change-Id: I43ce060e568a69f9842fbfad6f5fd62361ab2022
diff --git a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html
similarity index 60%
rename from polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html
rename to polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html
index 40379e4..3106fc8 100644
--- a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html
@@ -15,33 +15,28 @@
 limitations under the License.
 -->
 
+<script src="../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
+
 <script>
 (function(window) {
   'use strict';
 
-  const ANONYMOUS_NAME = 'Anonymous';
-
   window.Gerrit = window.Gerrit || {};
 
-  /** @polymerBehavior Gerrit.AnonymousNameBehavior */
-  Gerrit.AnonymousNameBehavior = {
+  /** @polymerBehavior Gerrit.DisplayNameBehavior */
+  Gerrit.DisplayNameBehavior = {
+    // TODO(dmfilippov) replace DisplayNameBehavior with GrDisplayNameUtils
+
     /**
      * enableEmail when true enables to fallback to using email if
      * the account name is not avilable.
      */
     getUserName(config, account, enableEmail) {
-      if (account && account.name) {
-        return account.name;
-      } else if (account && account.username) {
-        return account.username;
-      } else if (enableEmail && account && account.email) {
-        return account.email;
-      } else if (config && config.user &&
-          config.user.anonymous_coward_name !== 'Anonymous Coward') {
-        return config.user.anonymous_coward_name;
-      }
+      return GrDisplayNameUtils.getUserName(config, account, enableEmail);
+    },
 
-      return ANONYMOUS_NAME;
+    getGroupDisplayName(group) {
+      return GrDisplayNameUtils.getGroupDisplayName(group);
     },
   };
 })(window);
diff --git a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
similarity index 79%
rename from polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
rename to polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
index 64f0b3a..4c5c899 100644
--- a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
@@ -17,14 +17,14 @@
 -->
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-anonymous-name-behavior</title>
+<title>gr-display-name-behavior</title>
 <script src="/test/common-test-setup.js"></script>
 <script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
 <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
 <script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="gr-anonymous-name-behavior.html">
+<link rel="import" href="gr-display-name-behavior.html">
 
 <test-fixture id="basic">
   <template>
@@ -33,7 +33,7 @@
 </test-fixture>
 
 <script>
-  suite('gr-anonymous-name-behavior tests', () => {
+  suite('gr-display-name-behavior tests', () => {
     let element;
     // eslint-disable-next-line no-unused-vars
     const config = {
@@ -48,7 +48,7 @@
         is: 'test-element-anon',
         _legacyUndefinedCheck: true,
         behaviors: [
-          Gerrit.AnonymousNameBehavior,
+          Gerrit.DisplayNameBehavior,
         ],
       });
     });
@@ -57,21 +57,21 @@
       element = fixture('basic');
     });
 
-    test('test for it to return name', () => {
+    test('getUserName name only', () => {
       const account = {
         name: 'test-name',
       };
       assert.deepEqual(element.getUserName(config, account, true), 'test-name');
     });
 
-    test('test for it to return username', () => {
+    test('getUserName username only', () => {
       const account = {
         username: 'test-user',
       };
       assert.deepEqual(element.getUserName(config, account, true), 'test-user');
     });
 
-    test('test for it to return email', () => {
+    test('getUserName email only', () => {
       const account = {
         email: 'test-user@test-url.com',
       };
@@ -79,11 +79,11 @@
           'test-user@test-url.com');
     });
 
-    test('test for it not to Anonymous Coward as the anon name', () => {
+    test('getUserName returns not Anonymous Coward as the anon name', () => {
       assert.deepEqual(element.getUserName(config, null, true), 'Anonymous');
     });
 
-    test('test for the config returning the anon name', () => {
+    test('getUserName for the config returning the anon name', () => {
       const config = {
         user: {
           anonymous_coward_name: 'Test Anon',
@@ -91,5 +91,10 @@
       };
       assert.deepEqual(element.getUserName(config, null, true), 'Test Anon');
     });
+
+    test('getGroupDisplayName', () => {
+      assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
+          'Some user name (group)');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
deleted file mode 100644
index 1cb1ca5..0000000
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
+++ /dev/null
@@ -1,186 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-(function() {
-  'use strict';
-
-  Polymer({
-    is: 'gr-account-entry',
-    _legacyUndefinedCheck: true,
-
-    /**
-     * Fired when an account is entered.
-     *
-     * @event add
-     */
-
-    /**
-     * When allowAnyInput is true, account-text-changed is fired when input text
-     * changed. This is needed so that the reply dialog's save button can be
-     * enabled for arbitrary cc's, which don't need a 'commit'.
-     *
-     * @event account-text-changed
-     */
-    properties: {
-      allowAnyInput: Boolean,
-      borderless: Boolean,
-      change: Object,
-      filter: Function,
-      placeholder: String,
-      /**
-       * When true, account-entry uses the account suggest API endpoint, which
-       * suggests any account in that Gerrit instance (and does not suggest
-       * groups).
-       *
-       * When false/undefined, account-entry uses the suggest_reviewers API
-       * endpoint, which suggests any account or group in that Gerrit instance
-       * that is not already a reviewer (or is not CCed) on that change.
-       */
-      allowAnyUser: Boolean,
-
-      // suggestFrom = 0 to enable default suggestions.
-      suggestFrom: {
-        type: Number,
-        value: 0,
-      },
-
-      query: {
-        type: Function,
-        value() {
-          return this._getReviewerSuggestions.bind(this);
-        },
-      },
-
-      _config: Object,
-      /** The value of the autocomplete entry. */
-      _inputText: {
-        type: String,
-        observer: '_inputTextChanged',
-      },
-
-      _loggedIn: Boolean,
-    },
-
-    behaviors: [
-      Gerrit.AnonymousNameBehavior,
-      Gerrit.FireBehavior,
-    ],
-
-    attached() {
-      this.$.restAPI.getConfig().then(cfg => {
-        this._config = cfg;
-      });
-      this.$.restAPI.getLoggedIn().then(loggedIn => {
-        this._loggedIn = loggedIn;
-      });
-    },
-
-    get focusStart() {
-      return this.$.input.focusStart;
-    },
-
-    focus() {
-      this.$.input.focus();
-    },
-
-    clear() {
-      this.$.input.clear();
-    },
-
-    setText(text) {
-      this.$.input.setText(text);
-    },
-
-    getText() {
-      return this.$.input.text;
-    },
-
-    _handleInputCommit(e) {
-      this.fire('add', {value: e.detail.value});
-      this.$.input.focus();
-    },
-
-    _accountOrAnon(reviewer) {
-      return this.getUserName(this._config, reviewer, false);
-    },
-
-    _inputTextChanged(text) {
-      if (text.length && this.allowAnyInput) {
-        this.dispatchEvent(new CustomEvent(
-            'account-text-changed', {bubbles: true, composed: true}));
-      }
-    },
-
-    _makeSuggestion(reviewer) {
-      let name;
-      let value;
-      const generateStatusStr = function(account) {
-        return account.status ? '(' + account.status + ')' : '';
-      };
-      if (reviewer.account) {
-        // Reviewer is an account suggestion from getChangeSuggestedReviewers.
-        const reviewerName = this._accountOrAnon(reviewer.account);
-        const reviewerEmail = this._reviewerEmail(reviewer.account.email);
-        const reviewerStatus = generateStatusStr(reviewer.account);
-        name = [reviewerName, reviewerEmail, reviewerStatus]
-            .filter(p => p.length > 0).join(' ');
-        value = reviewer;
-      } else if (reviewer.group) {
-        // Reviewer is a group suggestion from getChangeSuggestedReviewers.
-        name = reviewer.group.name + ' (group)';
-        value = reviewer;
-      } else if (reviewer._account_id) {
-        // Reviewer is an account suggestion from getSuggestedAccounts.
-        const reviewerName = this._accountOrAnon(reviewer);
-        const reviewerEmail = this._reviewerEmail(reviewer.email);
-        const reviewerStatus = generateStatusStr(reviewer);
-        name = [reviewerName, reviewerEmail, reviewerStatus]
-            .filter(p => p.length > 0).join(' ');
-        value = {account: reviewer, count: 1};
-      }
-      return {name, value};
-    },
-
-    _getReviewerSuggestions(input) {
-      if (!this.change || !this.change._number || !this._loggedIn) {
-        return Promise.resolve([]);
-      }
-
-      const api = this.$.restAPI;
-      const xhr = this.allowAnyUser ?
-          api.getSuggestedAccounts(`cansee:${this.change._number} ${input}`) :
-          api.getChangeSuggestedReviewers(this.change._number, input);
-
-      return xhr.then(reviewers => {
-        if (!reviewers) { return []; }
-        if (!this.filter) {
-          return reviewers.map(this._makeSuggestion.bind(this));
-        }
-        return reviewers
-            .filter(this.filter)
-            .map(this._makeSuggestion.bind(this));
-      });
-    },
-
-    _reviewerEmail(email) {
-      if (typeof email !== 'undefined') {
-        return '<' + email + '>';
-      }
-
-      return '';
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
deleted file mode 100644
index 57bdd1d..0000000
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
+++ /dev/null
@@ -1,276 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-account-entry</title>
-<script src="/test/common-test-setup.js"></script>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-account-entry.html">
-
-<script>void(0);</script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-account-entry></gr-account-entry>
-  </template>
-</test-fixture>
-
-<script>
-  suite('gr-account-entry tests', () => {
-    let sandbox;
-    let _nextAccountId = 0;
-    const makeAccount = function(opt_status) {
-      const accountId = ++_nextAccountId;
-      return {
-        _account_id: accountId,
-        name: 'name ' + accountId,
-        email: 'email ' + accountId,
-        status: opt_status,
-      };
-    };
-    let _nextAccountId2 = 0;
-    const makeAccount2 = function(opt_status) {
-      const accountId2 = ++_nextAccountId2;
-      return {
-        _account_id: accountId2,
-        email: 'email ' + accountId2,
-        status: opt_status,
-      };
-    };
-    let _nextAccountId3 = 0;
-    const makeAccount3 = function(opt_status) {
-      const accountId3 = ++_nextAccountId3;
-      return {
-        _account_id: accountId3,
-        name: 'name ' + accountId3,
-        status: opt_status,
-      };
-    };
-
-    let owner;
-    let existingReviewer1;
-    let existingReviewer2;
-    let suggestion1;
-    let suggestion2;
-    let suggestion3;
-    let element;
-
-    setup(done => {
-      owner = makeAccount();
-      existingReviewer1 = makeAccount();
-      existingReviewer2 = makeAccount();
-      suggestion1 = {account: makeAccount()};
-      suggestion2 = {account: makeAccount()};
-      suggestion3 = {
-        group: {
-          id: 'suggested group id',
-          name: 'suggested group',
-        },
-      };
-
-      stub('gr-rest-api-interface', {
-        getLoggedIn() { return Promise.resolve(true); },
-      });
-
-      element = fixture('basic');
-      element.change = {
-        _number: 42,
-        owner,
-        reviewers: {
-          CC: [existingReviewer1],
-          REVIEWER: [existingReviewer2],
-        },
-      };
-      sandbox = sinon.sandbox.create();
-      return flush(done);
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    suite('stubbed values for _getReviewerSuggestions', () => {
-      setup(() => {
-        stub('gr-rest-api-interface', {
-          getChangeSuggestedReviewers() {
-            const redundantSuggestion1 = {account: existingReviewer1};
-            const redundantSuggestion2 = {account: existingReviewer2};
-            const redundantSuggestion3 = {account: owner};
-            return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
-              redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
-          },
-        });
-      });
-
-      test('_makeSuggestion formats account or group accordingly', () => {
-        let account = makeAccount();
-        const account2 = makeAccount2();
-        const account3 = makeAccount3();
-        let suggestion = element._makeSuggestion({account});
-        assert.deepEqual(suggestion, {
-          name: account.name + ' <' + account.email + '>',
-          value: {account},
-        });
-
-        const group = {name: 'test'};
-        suggestion = element._makeSuggestion({group});
-        assert.deepEqual(suggestion, {
-          name: group.name + ' (group)',
-          value: {group},
-        });
-
-        suggestion = element._makeSuggestion(account);
-        assert.deepEqual(suggestion, {
-          name: account.name + ' <' + account.email + '>',
-          value: {account, count: 1},
-        });
-
-        element._config = {
-          user: {
-            anonymous_coward_name: 'Anonymous Coward',
-          },
-        };
-        assert.deepEqual(element._accountOrAnon(account2), 'Anonymous');
-
-        account = makeAccount('OOO');
-
-        suggestion = element._makeSuggestion({account});
-        assert.deepEqual(suggestion, {
-          name: account.name + ' <' + account.email + '> (OOO)',
-          value: {account},
-        });
-
-        suggestion = element._makeSuggestion(account);
-        assert.deepEqual(suggestion, {
-          name: account.name + ' <' + account.email + '> (OOO)',
-          value: {account, count: 1},
-        });
-
-        sandbox.stub(element, '_reviewerEmail',
-            () => { return ''; });
-
-        suggestion = element._makeSuggestion(account3);
-        assert.deepEqual(suggestion, {
-          name: account3.name,
-          value: {account: account3, count: 1},
-        });
-      });
-
-      test('_reviewerEmail', () => {
-        assert.equal(
-            element._reviewerEmail('email@gerritreview.com'),
-            '<email@gerritreview.com>');
-        assert.equal(element._reviewerEmail(undefined), '');
-      });
-
-      test('_getReviewerSuggestions excludes owner+reviewers', done => {
-        element._getReviewerSuggestions().then(reviewers => {
-          // Default is no filtering.
-          assert.equal(reviewers.length, 6);
-
-          // Set up filter that only accepts suggestion1.
-          const accountId = suggestion1.account._account_id;
-          element.filter = function(suggestion) {
-            return suggestion.account &&
-                suggestion.account._account_id === accountId;
-          };
-
-          element._getReviewerSuggestions().then(reviewers => {
-            assert.deepEqual(reviewers, [element._makeSuggestion(suggestion1)]);
-          }).then(done);
-        });
-      });
-
-      test('_getReviewerSuggestions short circuits when logged out', () => {
-        // API call is already stubbed.
-        const xhrSpy = element.$.restAPI.getChangeSuggestedReviewers;
-        element._loggedIn = false;
-        return element._getReviewerSuggestions('').then(() => {
-          assert.isFalse(xhrSpy.called);
-          element._loggedIn = true;
-          return element._getReviewerSuggestions('').then(() => {
-            assert.isTrue(xhrSpy.called);
-          });
-        });
-      });
-    });
-
-    test('allowAnyUser', done => {
-      const suggestReviewerStub =
-          sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers')
-          .returns(Promise.resolve([]));
-      const suggestAccountStub =
-          sandbox.stub(element.$.restAPI, 'getSuggestedAccounts')
-          .returns(Promise.resolve([]));
-
-      element._getReviewerSuggestions('').then(() => {
-        assert.isTrue(suggestReviewerStub.calledOnce);
-        assert.isTrue(suggestReviewerStub.calledWith(42, ''));
-        assert.isFalse(suggestAccountStub.called);
-        element.allowAnyUser = true;
-
-        element._getReviewerSuggestions('').then(() => {
-          assert.isTrue(suggestReviewerStub.calledOnce);
-          assert.isTrue(suggestAccountStub.calledOnce);
-          assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
-          done();
-        });
-      });
-    });
-    test('account-text-changed fired when input text changed and allowAnyInput',
-        () => {
-          // Spy on query, as that is called when _updateSuggestions proceeds.
-          const changeStub = sandbox.stub();
-          element.allowAnyInput = true;
-          sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers')
-              .returns(Promise.resolve([]));
-          element.addEventListener('account-text-changed', changeStub);
-          element.$.input.text = 'a';
-          assert.isTrue(changeStub.calledOnce);
-          element.$.input.text = 'ab';
-          assert.isTrue(changeStub.calledTwice);
-        });
-
-    test('account-text-changed not fired when input text changed without ' +
-        'allowAnyUser', () => {
-          // Spy on query, as that is called when _updateSuggestions proceeds.
-      const changeStub = sandbox.stub();
-      sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers')
-          .returns(Promise.resolve([]));
-      element.addEventListener('account-text-changed', changeStub);
-      element.$.input.text = 'a';
-      assert.isFalse(changeStub.called);
-    });
-
-    test('setText', () => {
-      // Spy on query, as that is called when _updateSuggestions proceeds.
-      const suggestSpy = sandbox.spy(element.$.input, 'query');
-      element.setText('test text');
-      flushAsynchronousOperations();
-
-      assert.equal(element.$.input.$.input.value, 'test text');
-      assert.isFalse(suggestSpy.called);
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 15892c9..ee35583 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -36,6 +36,8 @@
 <link rel="import" href="../gr-change-requirements/gr-change-requirements.html">
 <link rel="import" href="../gr-commit-info/gr-commit-info.html">
 <link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
+<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script>
 
 <dom-module id="gr-change-metadata">
   <template>
@@ -172,9 +174,9 @@
               id="assigneeValue"
               placeholder="Set assignee..."
               accounts="{{_assignee}}"
-              change="[[change]]"
               readonly="[[_computeAssigneeReadOnly(_mutable, change)]]"
-              allow-any-user></gr-account-list>
+              suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
+          </gr-account-list>
         </span>
       </section>
       <section>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 6ee5a24..62446e4 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -471,5 +471,12 @@
       // dom-if.
       this.$$('.topicEditableLabel').open();
     },
+
+    _getReviewerSuggestionsProvider(change) {
+      const provider = new GrReviewerSuggestionsProvider(this.$.restAPI,
+          change._number, true);
+      provider.init();
+      return provider;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 08a3ec3..38c4a96 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -32,9 +32,11 @@
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../gr-account-list/gr-account-list.html">
+<link rel="import" href="../../shared/gr-account-list/gr-account-list.html">
 <link rel="import" href="../gr-label-scores/gr-label-scores.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script>
 
 <dom-module id="gr-reply-dialog">
   <template>
@@ -165,11 +167,11 @@
               id="reviewers"
               accounts="{{_reviewers}}"
               removable-values="[[change.removable_reviewers]]"
-              change="[[change]]"
               filter="[[filterReviewerSuggestion]]"
               pending-confirmation="{{_reviewerPendingConfirmation}}"
               placeholder="Add reviewer..."
-              on-account-text-changed="_handleAccountTextEntry">
+              on-account-text-changed="_handleAccountTextEntry"
+              suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
           </gr-account-list>
         </div>
         <div class="peopleList">
@@ -177,12 +179,12 @@
           <gr-account-list
               id="ccs"
               accounts="{{_ccs}}"
-              change="[[change]]"
               filter="[[filterCCSuggestion]]"
               pending-confirmation="{{_ccPendingConfirmation}}"
               allow-any-input
               placeholder="Add CC..."
-              on-account-text-changed="_handleAccountTextEntry">
+              on-account-text-changed="_handleAccountTextEntry"
+              suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
           </gr-account-list>
         </div>
         <gr-overlay
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 3b97439..6b7359f 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -895,5 +895,12 @@
     _sendDisabledChanged(sendDisabled) {
       this.dispatchEvent(new CustomEvent('send-disabled-changed'));
     },
+
+    _getReviewerSuggestionsProvider(change) {
+      const provider = new GrReviewerSuggestionsProvider(this.$.restAPI,
+          change._number, false);
+      provider.init();
+      return provider;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
index 7949002..dc18f8a 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
+<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
 <link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 2ade782..f8288c6 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -58,7 +58,7 @@
     },
 
     behaviors: [
-      Gerrit.AnonymousNameBehavior,
+      Gerrit.DisplayNameBehavior,
     ],
 
     detached() {
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
index 06e354c..c4ae41b 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
@@ -16,7 +16,7 @@
 -->
 <link rel="import" href="/bower_components/polymer/polymer.html">
 
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
+<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-search-bar/gr-search-bar.html">
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
index fed02d6..017310d 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -49,7 +49,7 @@
     },
 
     behaviors: [
-      Gerrit.AnonymousNameBehavior,
+      Gerrit.DisplayNameBehavior,
     ],
 
     attached() {
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
similarity index 79%
rename from polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
rename to polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
index 03fc606..ae656fd 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
@@ -15,12 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
 <link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-autocomplete/gr-autocomplete.html">
+<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-account-entry">
   <template>
@@ -36,14 +35,13 @@
         borderless="[[borderless]]"
         placeholder="[[placeholder]]"
         threshold="[[suggestFrom]]"
-        query="[[query]]"
+        query="[[querySuggestions]]"
         allow-non-suggested-values="[[allowAnyInput]]"
         on-commit="_handleInputCommit"
         clear-on-commit
         warn-uncommitted
         text="{{_inputText}}">
     </gr-autocomplete>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-account-entry.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
new file mode 100644
index 0000000..63bd252
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
@@ -0,0 +1,102 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function() {
+  'use strict';
+
+  /**
+   * gr-account-entry is an element for entering account
+   * and/or group with autocomplete support.
+   */
+  Polymer({
+    is: 'gr-account-entry',
+    _legacyUndefinedCheck: true,
+
+    /**
+     * Fired when an account is entered.
+     *
+     * @event add
+     */
+
+    /**
+     * When allowAnyInput is true, account-text-changed is fired when input text
+     * changed. This is needed so that the reply dialog's save button can be
+     * enabled for arbitrary cc's, which don't need a 'commit'.
+     *
+     * @event account-text-changed
+     */
+    properties: {
+      allowAnyInput: Boolean,
+      borderless: Boolean,
+      placeholder: String,
+
+      // suggestFrom = 0 to enable default suggestions.
+      suggestFrom: {
+        type: Number,
+        value: 0,
+      },
+
+      /** @type {!function(string): !Promise<Array<{name, value}>>} */
+      querySuggestions: {
+        type: Function,
+        notify: true,
+        value() {
+          return input => Promise.resolve([]);
+        },
+      },
+
+      _config: Object,
+      /** The value of the autocomplete entry. */
+      _inputText: {
+        type: String,
+        observer: '_inputTextChanged',
+      },
+
+    },
+
+    get focusStart() {
+      return this.$.input.focusStart;
+    },
+
+    focus() {
+      this.$.input.focus();
+    },
+
+    clear() {
+      this.$.input.clear();
+    },
+
+    setText(text) {
+      this.$.input.setText(text);
+    },
+
+    getText() {
+      return this.$.input.text;
+    },
+
+    _handleInputCommit(e) {
+      this.fire('add', {value: e.detail.value});
+      this.$.input.focus();
+    },
+
+    _inputTextChanged(text) {
+      if (text.length && this.allowAnyInput) {
+        this.dispatchEvent(new CustomEvent(
+                'account-text-changed', {bubbles: true, composed: true}));
+      }
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
new file mode 100644
index 0000000..59792a7
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-account-entry</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<script src="../../../scripts/util.js"></script>
+
+<link rel="import" href="gr-account-entry.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-account-entry></gr-account-entry>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-account-entry tests', () => {
+    let sandbox;
+
+    const suggestion1 = {
+      email: 'email1@example.com',
+      _account_id: 1,
+      some_property: 'value',
+    };
+    const suggestion2 = {
+      email: 'email2@example.com',
+      _account_id: 2,
+    };
+    const suggestion3 = {
+      email: 'email25@example.com',
+      _account_id: 25,
+      some_other_property: 'other value',
+    };
+
+    setup(done => {
+      element = fixture('basic');
+      sandbox = sinon.sandbox.create();
+      return flush(done);
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    suite('stubbed values for querySuggestions', () => {
+      setup(() => {
+        element.querySuggestions = input => {
+          return Promise.resolve([
+            suggestion1,
+            suggestion2,
+            suggestion3,
+          ]);
+        };
+      });
+    });
+
+    test('account-text-changed fired when input text changed and allowAnyInput',
+        () => {
+          // Spy on query, as that is called when _updateSuggestions proceeds.
+          const changeStub = sandbox.stub();
+          element.allowAnyInput = true;
+          element.querySuggestions = input => Promise.resolve([]);
+          element.addEventListener('account-text-changed', changeStub);
+          element.$.input.text = 'a';
+          assert.isTrue(changeStub.calledOnce);
+          element.$.input.text = 'ab';
+          assert.isTrue(changeStub.calledTwice);
+        });
+
+    test('account-text-changed not fired when input text changed without ' +
+        'allowAnyInput', () => {
+          // Spy on query, as that is called when _updateSuggestions proceeds.
+      const changeStub = sandbox.stub();
+      element.querySuggestions = input => Promise.resolve([]);
+      element.addEventListener('account-text-changed', changeStub);
+      element.$.input.text = 'a';
+      assert.isFalse(changeStub.called);
+    });
+
+    test('setText', () => {
+      // Spy on query, as that is called when _updateSuggestions proceeds.
+      const suggestSpy = sandbox.spy(element.$.input, 'query');
+      element.setText('test text');
+      flushAsynchronousOperations();
+
+      assert.equal(element.$.input.$.input.value, 'test text');
+      assert.isFalse(suggestSpy.called);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
index 9d0782f..f807a74 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -15,7 +15,7 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
+<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
index 88df33b..a778e8b 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -52,7 +52,7 @@
     },
 
     behaviors: [
-      Gerrit.AnonymousNameBehavior,
+      Gerrit.DisplayNameBehavior,
       Gerrit.TooltipBehavior,
     ],
 
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html
similarity index 91%
rename from polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
rename to polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html
index 31c1be5..c793c07 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html
@@ -17,7 +17,7 @@
 
 <link rel="import" href="/bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
+<link rel="import" href="../gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../gr-account-entry/gr-account-entry.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -56,7 +56,7 @@
             account="[[account]]"
             class$="[[_computeChipClass(account)]]"
             data-account-id$="[[account._account_id]]"
-            removable="[[_computeRemovable(account)]]"
+            removable="[[_computeRemovable(account, readonly)]]"
             on-keydown="_handleChipKeydown"
             tabindex="-1">
         </gr-account-chip>
@@ -67,13 +67,13 @@
         hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]"
         id="entry"
         change="[[change]]"
-        filter="[[filter]]"
         placeholder="[[placeholder]]"
         on-add="_handleAdd"
         on-input-keydown="_handleInputKeydown"
         allow-any-input="[[allowAnyInput]]"
-        allow-any-user="[[allowAnyUser]]">
+        query-suggestions="[[_querySuggestions]]">
     </gr-account-entry>
+    <slot></slot>
   </template>
   <script src="gr-account-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
similarity index 79%
rename from polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
rename to polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
index 479fee2..9edc9c8 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -19,6 +19,24 @@
 
   const VALID_EMAIL_ALERT = 'Please input a valid email.';
 
+  const Defs = {};
+
+  /**
+   * @typedef {{
+   *   name: string,
+   *   value: Object,
+   * }}
+   */
+  Defs.GrSuggestionItem;
+
+  /**
+   * @typedef {{
+   *    getSuggestions: function(string): Promise<Array<Object>>,
+   *    makeSuggestionItem: function(Object): Defs.GrSuggestionItem,
+   * }}
+   */
+  Defs.GrSuggestionsProvider;
+
   Polymer({
     is: 'gr-account-list',
     _legacyUndefinedCheck: true,
@@ -38,6 +56,19 @@
       change: Object,
       filter: Function,
       placeholder: String,
+      disabled: {
+        type: Function,
+        value: false,
+      },
+
+      /**
+       * Returns suggestions and convert them to list item
+       * @type {Defs.GrSuggestionsProvider}
+       */
+      suggestionsProvider: {
+        type: Object,
+      },
+
       /**
        * Needed for template checking since value is initially set to null.
        * @type {?Object} */
@@ -50,21 +81,6 @@
         type: Boolean,
         value: false,
       },
-
-      /**
-       * When true, the account-entry autocomplete uses the account suggest API
-       * endpoint, which suggests any account in that Gerrit instance (and does
-       * not suggest groups).
-       *
-       * When false/undefined, account-entry uses the suggest_reviewers API
-       * endpoint, which suggests any account or group in that Gerrit instance
-       * that is not already a reviewer (or is not CCed) on that change.
-       */
-      allowAnyUser: {
-        type: Boolean,
-        value: false,
-      },
-
       /**
        * When true, allows for non-suggested inputs to be added.
        */
@@ -82,6 +98,16 @@
         type: Number,
         value: 0,
       },
+
+      /** Returns suggestion items
+      * @type {!function(string): Promise<Array<Defs.GrSuggestionItem>>}
+      */
+      _querySuggestions: {
+        type: Function,
+        value() {
+          return this._getSuggestions.bind(this);
+        },
+      },
     },
 
     behaviors: [
@@ -103,31 +129,46 @@
       return this.$.entry.focusStart;
     },
 
-    _handleAdd(e) {
-      this._addReviewer(e.detail.value);
+    _getSuggestions(input) {
+      const provider = this.suggestionsProvider;
+      if (!provider) {
+        return Promise.resolve([]);
+      }
+      return provider.getSuggestions(input).then(suggestions => {
+        if (!suggestions) { return []; }
+        if (this.filter) {
+          suggestions = suggestions.filter(this.filter);
+        }
+        return suggestions.map(suggestion =>
+            provider.makeSuggestionItem(suggestion));
+      });
     },
 
-    _addReviewer(reviewer) {
+    _handleAdd(e) {
+      this._addAccountItem(e.detail.value);
+    },
+
+    _addAccountItem(item) {
       // Append new account or group to the accounts property. We add our own
       // internal properties to the account/group here, so we clone the object
       // to avoid cluttering up the shared change object.
-      if (reviewer.account) {
+      if (item.account) {
         const account =
-            Object.assign({}, reviewer.account, {_pendingAdd: true});
+            Object.assign({}, item.account, {_pendingAdd: true});
         this.push('accounts', account);
-      } else if (reviewer.group) {
-        if (reviewer.confirm) {
-          this.pendingConfirmation = reviewer;
+      } else if (item.group) {
+        if (item.confirm) {
+          this.pendingConfirmation = item;
           return;
         }
-        const group = Object.assign({}, reviewer.group,
+        const group = Object.assign({}, item.group,
             {_pendingAdd: true, _group: true});
         this.push('accounts', group);
       } else if (this.allowAnyInput) {
-        if (!reviewer.includes('@')) {
+        if (!item.includes('@')) {
           // Repopulate the input with what the user tried to enter and have
           // a toast tell them why they can't enter it.
-          this.$.entry.setText(reviewer);
+          this.$.entry.setText(item);
           this.dispatchEvent(new CustomEvent('show-alert', {
             detail: {message: VALID_EMAIL_ALERT},
             bubbles: true,
@@ -135,7 +176,7 @@
           }));
           return false;
         } else {
-          const account = {email: reviewer, _pendingAdd: true};
+          const account = {email: item, _pendingAdd: true};
           this.push('accounts', account);
         }
       }
@@ -173,8 +214,8 @@
       return a === b;
     },
 
-    _computeRemovable(account) {
-      if (this.readonly) { return false; }
+    _computeRemovable(account, readonly) {
+      if (readonly) { return false; }
       if (this.removableValues) {
         for (let i = 0; i < this.removableValues.length; i++) {
           if (this._accountMatches(this.removableValues[i], account)) {
@@ -193,7 +234,9 @@
     },
 
     _removeAccount(toRemove) {
-      if (!toRemove || !this._computeRemovable(toRemove)) { return; }
+      if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
+        return;
+      }
       for (let i = 0; i < this.accounts.length; i++) {
         let matches;
         const account = this.accounts[i];
@@ -277,7 +320,7 @@
     submitEntryText() {
       const text = this.$.entry.getText();
       if (!text.length) { return true; }
-      const wasSubmitted = this._addReviewer(text);
+      const wasSubmitted = this._addAccountItem(text);
       if (wasSubmitted) { this.$.entry.clear(); }
       return wasSubmitted;
     },
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
similarity index 77%
rename from polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
rename to polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
index d32c269..22e3a3d 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
@@ -35,6 +35,15 @@
 </test-fixture>
 
 <script>
+  class MockSuggestionsProvider {
+    getSuggestions(input) {
+      return Promise.resolve([]);
+    }
+
+    makeSuggestionItem(item) {
+      return item;
+    }
+  }
   suite('gr-account-list tests', () => {
     let _nextAccountId = 0;
     const makeAccount = function() {
@@ -51,10 +60,11 @@
       };
     };
 
-    let existingReviewer1;
-    let existingReviewer2;
+    let existingAccount1;
+    let existingAccount2;
     let sandbox;
     let element;
+    let suggestionsProvider;
 
     function getChips() {
       return Polymer.dom(element.root).querySelectorAll('gr-account-chip');
@@ -62,14 +72,16 @@
 
     setup(() => {
       sandbox = sinon.sandbox.create();
-      existingReviewer1 = makeAccount();
-      existingReviewer2 = makeAccount();
+      existingAccount1 = makeAccount();
+      existingAccount2 = makeAccount();
 
       stub('gr-rest-api-interface', {
         getConfig() { return Promise.resolve({}); },
       });
       element = fixture('basic');
-      element.accounts = [existingReviewer1, existingReviewer2];
+      element.accounts = [existingAccount1, existingAccount2];
+      suggestionsProvider = new MockSuggestionsProvider();
+      element.suggestionsProvider = suggestionsProvider;
     });
 
     teardown(() => {
@@ -109,7 +121,7 @@
       assert.isTrue(chips[2].classList.contains('pendingAdd'));
 
       // Removed accounts are taken out of the list.
-      element.fire('remove', {account: existingReviewer1});
+      element.fire('remove', {account: existingAccount1});
       flushAsynchronousOperations();
       chips = getChips();
       assert.equal(chips.length, 2);
@@ -117,7 +129,7 @@
       assert.isTrue(chips[1].classList.contains('pendingAdd'));
 
       // Invalid remove is ignored.
-      element.fire('remove', {account: existingReviewer1});
+      element.fire('remove', {account: existingAccount1});
       element.fire('remove', {account: newAccount});
       flushAsynchronousOperations();
       chips = getChips();
@@ -147,6 +159,52 @@
       assert.isFalse(chips[0].classList.contains('pendingAdd'));
     });
 
+    test('_getSuggestions uses filter correctly', done => {
+      const originalSuggestions = [
+        {
+          email: 'abc@example.com',
+          text: 'abcd',
+          _account_id: 3,
+        },
+        {
+          email: 'qwe@example.com',
+          text: 'qwer',
+          _account_id: 1,
+        },
+        {
+          email: 'xyz@example.com',
+          text: 'aaaaa',
+          _account_id: 25,
+        },
+      ];
+      sandbox.stub(suggestionsProvider, 'getSuggestions')
+          .returns(Promise.resolve(originalSuggestions));
+      sandbox.stub(suggestionsProvider, 'makeSuggestionItem', suggestion => {
+        return {
+          name: suggestion.email,
+          value: suggestion._account_id,
+        };
+      });
+
+
+      element._getSuggestions().then(suggestions => {
+        // Default is no filtering.
+        assert.equal(suggestions.length, 3);
+
+        // Set up filter that only accepts suggestion1.
+        const accountId = originalSuggestions[0]._account_id;
+        element.filter = function(suggestion) {
+          return suggestion._account_id === accountId;
+        };
+
+        element._getSuggestions().then(suggestions => {
+          assert.deepEqual(suggestions,
+              [{name: originalSuggestions[0].email,
+                value: originalSuggestions[0]._account_id}]);
+        }).then(done);
+      });
+    });
+
     test('_computeChipClass', () => {
       const account = makeAccount();
       assert.equal(element._computeChipClass(account), '');
@@ -163,18 +221,18 @@
       newAccount._pendingAdd = true;
       element.readonly = false;
       element.removableValues = [];
-      assert.isFalse(element._computeRemovable(existingReviewer1));
-      assert.isTrue(element._computeRemovable(newAccount));
+      assert.isFalse(element._computeRemovable(existingAccount1, false));
+      assert.isTrue(element._computeRemovable(newAccount, false));
 
 
-      element.removableValues = [existingReviewer1];
-      assert.isTrue(element._computeRemovable(existingReviewer1));
-      assert.isTrue(element._computeRemovable(newAccount));
-      assert.isFalse(element._computeRemovable(existingReviewer2));
+      element.removableValues = [existingAccount1];
+      assert.isTrue(element._computeRemovable(existingAccount1, false));
+      assert.isTrue(element._computeRemovable(newAccount, false));
+      assert.isFalse(element._computeRemovable(existingAccount2, false));
 
       element.readonly = true;
-      assert.isFalse(element._computeRemovable(existingReviewer1));
-      assert.isFalse(element._computeRemovable(newAccount));
+      assert.isFalse(element._computeRemovable(existingAccount1, true));
+      assert.isFalse(element._computeRemovable(newAccount, true));
     });
 
     test('submitEntryText', () => {
@@ -293,13 +351,40 @@
       assert.isTrue(element.$.entry.hasAttribute('hidden'));
     });
 
-    suite('allowAnyInput', () => {
-      let entry;
+    test('enter text calls suggestions provider', done => {
+      const suggestions = [
+        {
+          email: 'abc@example.com',
+          text: 'abcd',
+        },
+        {
+          email: 'qwe@example.com',
+          text: 'qwer',
+        },
+      ];
+      const getSuggestionsStub =
+          sandbox.stub(suggestionsProvider, 'getSuggestions')
+              .returns(Promise.resolve(suggestions));
 
+      const makeSuggestionItemStub =
+          sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
+
+      const input = element.$.entry.$.input;
+
+      input.text = 'newTest';
+      MockInteractions.focus(input.$.input);
+      input.noDebounce = true;
+      flushAsynchronousOperations();
+      flush(() => {
+        assert.isTrue(getSuggestionsStub.calledOnce);
+        assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest');
+        assert.equal(makeSuggestionItemStub.getCalls().length, 2);
+        done();
+      });
+    });
+
+    suite('allowAnyInput', () => {
       setup(() => {
-        entry = element.$.entry;
-        sandbox.stub(entry, '_getReviewerSuggestions');
-        sandbox.stub(entry.$.input, '_updateSuggestions');
         element.allowAnyInput = true;
       });
 
@@ -334,7 +419,6 @@
     suite('keyboard interactions', () => {
       test('backspace at text input start removes last account', () => {
         const input = element.$.entry.$.input;
-        sandbox.stub(element.$.entry, '_getReviewerSuggestions');
         sandbox.stub(input, '_updateSuggestions');
         sandbox.stub(element, '_computeRemovable').returns(true);
         // Next line is a workaround for Firefix not moving cursor
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js
new file mode 100644
index 0000000..238cf15
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js
@@ -0,0 +1,55 @@
+(function(window) {
+  'use strict';
+
+  if (window.GrDisplayNameUtils) {
+    return;
+  }
+
+  const ANONYMOUS_NAME = 'Anonymous';
+
+  class GrDisplayNameUtils {
+    /**
+     * enableEmail when true enables to fallback to using email if
+     * the account name is not avilable.
+     */
+    static getUserName(config, account, enableEmail) {
+      if (account && account.name) {
+        return account.name;
+      } else if (account && account.username) {
+        return account.username;
+      } else if (enableEmail && account && account.email) {
+        return account.email;
+      } else if (config && config.user &&
+          config.user.anonymous_coward_name !== 'Anonymous Coward') {
+        return config.user.anonymous_coward_name;
+      }
+
+      return ANONYMOUS_NAME;
+    }
+
+    static getAccountDisplayName(config, account, enableEmail) {
+      const reviewerName = this._accountOrAnon(config, account, enableEmail);
+      const reviewerEmail = this._accountEmail(account.email);
+      const reviewerStatus = account.status ? '(' + account.status + ')' : '';
+      return [reviewerName, reviewerEmail, reviewerStatus]
+          .filter(p => p.length > 0).join(' ');
+    }
+
+    static _accountOrAnon(config, reviewer, enableEmail) {
+      return this.getUserName(config, reviewer, !!enableEmail);
+    }
+
+    static _accountEmail(email) {
+      if (typeof email !== 'undefined') {
+        return '<' + email + '>';
+      }
+      return '';
+    }
+
+    static getGroupDisplayName(group) {
+      return group.name + ' (group)';
+    }
+  }
+
+  window.GrDisplayNameUtils = GrDisplayNameUtils;
+})(window);
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
new file mode 100644
index 0000000..25ca4c5
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-display-name-utils</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../test/common-test-setup.html"/>
+<script src="gr-display-name-utils.js"></script>
+
+<script>
+  suite('gr-display-name-utils tests', () => {
+    // eslint-disable-next-line no-unused-vars
+    const config = {
+      user: {
+        anonymous_coward_name: 'Anonymous Coward',
+      },
+    };
+
+
+    test('getUserName name only', () => {
+      const account = {
+        name: 'test-name',
+      };
+      assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+          'test-name');
+    });
+
+    test('getUserName username only', () => {
+      const account = {
+        username: 'test-user',
+      };
+      assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+          'test-user');
+    });
+
+    test('getUserName email only', () => {
+      const account = {
+        email: 'test-user@test-url.com',
+      };
+      assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+          'test-user@test-url.com');
+    });
+
+    test('getUserName returns not Anonymous Coward as the anon name', () => {
+      assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
+          'Anonymous');
+    });
+
+    test('getUserName for the config returning the anon name', () => {
+      const config = {
+        user: {
+          anonymous_coward_name: 'Test Anon',
+        },
+      };
+      assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
+          'Test Anon');
+    });
+
+    test('getAccountDisplayName - account with name only', () => {
+      assert.equal(
+          GrDisplayNameUtils.getAccountDisplayName(config,
+              {name: 'Some user name'}),
+          'Some user name');
+    });
+
+    test('getAccountDisplayName - account with email only', () => {
+      assert.equal(
+          GrDisplayNameUtils.getAccountDisplayName(config,
+              {email: 'my@example.com'}),
+          'Anonymous <my@example.com>');
+    });
+
+    test('getAccountDisplayName - account with email only - allowEmail', () => {
+      assert.equal(
+          GrDisplayNameUtils.getAccountDisplayName(config,
+              {email: 'my@example.com'}, true),
+          'my@example.com <my@example.com>');
+    });
+
+    test('getAccountDisplayName - account with name and status', () => {
+      assert.equal(
+          GrDisplayNameUtils.getAccountDisplayName(config, {
+            name: 'Some name',
+            status: 'OOO',
+          }),
+          'Some name (OOO)');
+    });
+
+    test('getAccountDisplayName - account with name and email', () => {
+      assert.equal(
+          GrDisplayNameUtils.getAccountDisplayName(config, {
+            name: 'Some name',
+            email: 'my@example.com',
+          }),
+          'Some name <my@example.com>');
+    });
+
+    test('getAccountDisplayName - account with name, email and status', () => {
+      assert.equal(
+          GrDisplayNameUtils.getAccountDisplayName(config, {
+            name: 'Some name',
+            email: 'my@example.com',
+            status: 'OOO',
+          }),
+          'Some name <my@example.com> (OOO)');
+    });
+
+    test('getGroupDisplayName', () => {
+      assert.equal(
+          GrDisplayNameUtils.getGroupDisplayName({name: 'Some user name'}),
+          'Some user name (group)');
+    });
+
+    test('_accountEmail', () => {
+      assert.equal(
+          GrDisplayNameUtils._accountEmail('email@gerritreview.com'),
+          '<email@gerritreview.com>');
+      assert.equal(GrDisplayNameUtils._accountEmail(undefined), '');
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
new file mode 100644
index 0000000..67001d2
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function(window) {
+  'use strict';
+
+  if (window.GrEmailSuggestionsProvider) {
+    return;
+  }
+
+  class GrEmailSuggestionsProvider {
+    constructor(restAPI) {
+      this._restAPI = restAPI;
+    }
+
+    getSuggestions(input) {
+      return this._restAPI.getSuggestedAccounts(`${input}`)
+          .then(accounts => {
+            if (!accounts) { return []; }
+            return accounts;
+          });
+    }
+
+    makeSuggestionItem(account) {
+      return {
+        name: GrDisplayNameUtils.getAccountDisplayName(null, account, true),
+        value: {account, count: 1},
+      };
+    }
+  }
+
+  window.GrEmailSuggestionsProvider = GrEmailSuggestionsProvider;
+})(window);
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
new file mode 100644
index 0000000..fb6b5d4
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-email-suggestions-provider</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
+<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="gr-email-suggestions-provider.js"></script>
+
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+</test-fixture>
+
+<script>
+  suite('GrEmailSuggestionsProvider tests', () => {
+    let sandbox;
+    let restAPI;
+    let provider;
+    const account1 = {
+      name: 'Some name',
+      email: 'some@example.com',
+    };
+    const account2 = {
+      email: 'other@example.com',
+      _account_id: 3,
+    };
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+
+      stub('gr-rest-api-interface', {
+        getConfig() { return Promise.resolve({}); },
+      });
+      restAPI = fixture('basic');
+      provider = new GrEmailSuggestionsProvider(restAPI);
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('getSuggestions', done => {
+      const getSuggestedAccountsStub =
+          sandbox.stub(restAPI, 'getSuggestedAccounts')
+            .returns(Promise.resolve([account1, account2]));
+
+      provider.getSuggestions('Some input').then(res => {
+        assert.deepEqual(res, [account1, account2]);
+        assert.isTrue(getSuggestedAccountsStub.calledOnce);
+        assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
+        done();
+      });
+    });
+
+    test('makeSuggestionItem', () => {
+      assert.deepEqual(provider.makeSuggestionItem(account1), {
+        name: 'Some name <some@example.com>',
+        value: {
+          account: account1,
+          count: 1,
+        },
+      });
+
+      assert.deepEqual(provider.makeSuggestionItem(account2), {
+        name: 'other@example.com <other@example.com>',
+        value: {
+          account: account2,
+          count: 1,
+        },
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js
new file mode 100644
index 0000000..a95670b
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function(window) {
+  'use strict';
+
+  if (window.GrGroupSuggestionsProvider) {
+    return;
+  }
+
+  class GrGroupSuggestionsProvider {
+    constructor(restAPI) {
+      this._restAPI = restAPI;
+    }
+
+    getSuggestions(input) {
+      return this._restAPI.getSuggestedGroups(`${input}`)
+          .then(groups => {
+            if (!groups) { return []; }
+            const keys = Object.keys(groups);
+            return keys.map(key => {
+              return Object.assign({}, groups[key], {name: key});
+            });
+          });
+    }
+
+    makeSuggestionItem(suggestion) {
+      return {name: suggestion.name,
+        value: {group: {name: suggestion.name, id: suggestion.id}}};
+    }
+  }
+
+  window.GrGroupSuggestionsProvider = GrGroupSuggestionsProvider;
+})(window);
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
new file mode 100644
index 0000000..b60aaa9
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-group-suggestions-provider</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
+<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="gr-group-suggestions-provider.js"></script>
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+</test-fixture>
+
+<script>
+  suite('GrGroupSuggestionsProvider tests', () => {
+    let sandbox;
+    let restAPI;
+    let provider;
+    const group1 = {
+      name: 'Some name',
+      id: 1,
+    };
+    const group2 = {
+      name: 'Other name',
+      id: 3,
+      url: 'abcd',
+    };
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+
+      stub('gr-rest-api-interface', {
+        getConfig() { return Promise.resolve({}); },
+      });
+      restAPI = fixture('basic');
+      provider = new GrGroupSuggestionsProvider(restAPI);
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('getSuggestions', done => {
+      const getSuggestedAccountsStub =
+          sandbox.stub(restAPI, 'getSuggestedGroups')
+              .returns(Promise.resolve({
+                'Some name': {id: 1},
+                'Other name': {id: 3, url: 'abcd'},
+              }));
+
+      provider.getSuggestions('Some input').then(res => {
+        assert.deepEqual(res, [group1, group2]);
+        assert.isTrue(getSuggestedAccountsStub.calledOnce);
+        assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
+        done();
+      });
+    });
+
+    test('makeSuggestionItem', () => {
+      assert.deepEqual(provider.makeSuggestionItem(group1), {
+        name: 'Some name',
+        value: {
+          group: {
+            name: 'Some name',
+            id: 1,
+          },
+        },
+      });
+
+      assert.deepEqual(provider.makeSuggestionItem(group2), {
+        name: 'Other name',
+        value: {
+          group: {
+            name: 'Other name',
+            id: 3,
+          },
+        },
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
new file mode 100644
index 0000000..7f1a9b1
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
@@ -0,0 +1,90 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function(window) {
+  'use strict';
+
+  if (window.GrReviewerSuggestionsProvider) {
+    return;
+  }
+
+  class GrReviewerSuggestionsProvider {
+    constructor(restAPI, changeNumber, allowAnyUser) {
+      this._changeNumber = changeNumber;
+      this._allowAnyUser = allowAnyUser;
+      this._restAPI = restAPI;
+    }
+
+    init() {
+      if (this._initPromise) {
+        return this._initPromise;
+      }
+      const getConfigPromise = this._restAPI.getConfig().then(cfg => {
+        this._config = cfg;
+      });
+      const getLoggedInPromise = this._restAPI.getLoggedIn().then(loggedIn => {
+        this._loggedIn = loggedIn;
+      });
+      this._initPromise = Promise.all([getConfigPromise, getLoggedInPromise])
+        .then(() => {
+          this._initialized = true;
+        });
+      return this._initPromise;
+    }
+
+    getSuggestions(input) {
+      if (!this._initialized || !this._loggedIn) {
+        return Promise.resolve([]);
+      }
+      const api = this._restAPI;
+      const xhr = this._allowAnyUser ?
+          api.getSuggestedAccounts(`cansee:${this._changeNumber} ${input}`) :
+          api.getChangeSuggestedReviewers(this._changeNumber, input);
+
+      return xhr.then(reviewers => (reviewers || []));
+    }
+
+    makeSuggestionItem(suggestion) {
+      if (suggestion.account) {
+        // Reviewer is an account suggestion from getChangeSuggestedReviewers.
+        return {
+          name: GrDisplayNameUtils.getAccountDisplayName(this._config,
+              suggestion.account, false),
+          value: suggestion,
+        };
+      }
+
+      if (suggestion.group) {
+        // Reviewer is a group suggestion from getChangeSuggestedReviewers.
+        return {
+          name: GrDisplayNameUtils.getGroupDisplayName(suggestion.group),
+          value: suggestion,
+        };
+      }
+
+      if (suggestion._account_id) {
+        // Reviewer is an account suggestion from getSuggestedAccounts.
+        return {
+          name: GrDisplayNameUtils.getAccountDisplayName(this._config,
+              suggestion, false),
+          value: {account: suggestion, count: 1},
+        };
+      }
+    }
+  }
+
+  window.GrReviewerSuggestionsProvider = GrReviewerSuggestionsProvider;
+})(window);
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
new file mode 100644
index 0000000..bb73520
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
@@ -0,0 +1,260 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-reviewer-suggestions-provider</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
+<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="gr-reviewer-suggestions-provider.js"></script>
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+</test-fixture>
+
+<script>
+  suite('GrReviewerSuggestionsProvider tests', () => {
+    let sandbox;
+    let _nextAccountId = 0;
+    const makeAccount = function(opt_status) {
+      const accountId = ++_nextAccountId;
+      return {
+        _account_id: accountId,
+        name: 'name ' + accountId,
+        email: 'email ' + accountId,
+        status: opt_status,
+      };
+    };
+    let _nextAccountId2 = 0;
+    const makeAccount2 = function(opt_status) {
+      const accountId2 = ++_nextAccountId2;
+      return {
+        _account_id: accountId2,
+        name: 'name ' + accountId2,
+        status: opt_status,
+      };
+    };
+
+    let owner;
+    let existingReviewer1;
+    let existingReviewer2;
+    let suggestion1;
+    let suggestion2;
+    let suggestion3;
+    let restAPI;
+    let provider;
+
+    let redundantSuggestion1;
+    let redundantSuggestion2;
+    let redundantSuggestion3;
+    let change;
+
+    setup(done => {
+      owner = makeAccount();
+      existingReviewer1 = makeAccount();
+      existingReviewer2 = makeAccount();
+      suggestion1 = {account: makeAccount()};
+      suggestion2 = {account: makeAccount()};
+      suggestion3 = {
+        group: {
+          id: 'suggested group id',
+          name: 'suggested group',
+        },
+      };
+
+      stub('gr-rest-api-interface', {
+        getLoggedIn() { return Promise.resolve(true); },
+        getConfig() { return Promise.resolve({}); },
+      });
+
+      restAPI = fixture('basic');
+      change = {
+        _number: 42,
+        owner,
+        reviewers: {
+          CC: [existingReviewer1],
+          REVIEWER: [existingReviewer2],
+        },
+      };
+      sandbox = sinon.sandbox.create();
+      return flush(done);
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+    suite('allowAnyUser set to false', () => {
+      setup(done => {
+        provider = new GrReviewerSuggestionsProvider(restAPI, change._number,
+            false);
+        provider.init().then(done);
+      });
+      suite('stubbed values for _getReviewerSuggestions', () => {
+        setup(() => {
+          stub('gr-rest-api-interface', {
+            getChangeSuggestedReviewers() {
+              redundantSuggestion1 = {account: existingReviewer1};
+              redundantSuggestion2 = {account: existingReviewer2};
+              redundantSuggestion3 = {account: owner};
+              return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
+                redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
+            },
+          });
+        });
+
+        test('makeSuggestionItem formats account or group accordingly', () => {
+          let account = makeAccount();
+          const account3 = makeAccount2();
+          let suggestion = provider.makeSuggestionItem({account});
+          assert.deepEqual(suggestion, {
+            name: account.name + ' <' + account.email + '>',
+            value: {account},
+          });
+
+          const group = {name: 'test'};
+          suggestion = provider.makeSuggestionItem({group});
+          assert.deepEqual(suggestion, {
+            name: group.name + ' (group)',
+            value: {group},
+          });
+
+          suggestion = provider.makeSuggestionItem(account);
+          assert.deepEqual(suggestion, {
+            name: account.name + ' <' + account.email + '>',
+            value: {account, count: 1},
+          });
+
+          suggestion = provider.makeSuggestionItem({account: {}});
+          assert.deepEqual(suggestion, {
+            name: 'Anonymous',
+            value: {account: {}},
+          });
+
+          provider._config = {
+            user: {
+              anonymous_coward_name: 'Anonymous Coward Name',
+            },
+          };
+
+          suggestion = provider.makeSuggestionItem({account: {}});
+          assert.deepEqual(suggestion, {
+            name: 'Anonymous Coward Name',
+            value: {account: {}},
+          });
+
+          account = makeAccount('OOO');
+
+          suggestion = provider.makeSuggestionItem({account});
+          assert.deepEqual(suggestion, {
+            name: account.name + ' <' + account.email + '> (OOO)',
+            value: {account},
+          });
+
+          suggestion = provider.makeSuggestionItem(account);
+          assert.deepEqual(suggestion, {
+            name: account.name + ' <' + account.email + '> (OOO)',
+            value: {account, count: 1},
+          });
+
+          sandbox.stub(GrDisplayNameUtils, '_accountEmail',
+              () => {
+                return '';
+              });
+
+          suggestion = provider.makeSuggestionItem(account3);
+          assert.deepEqual(suggestion, {
+            name: account3.name,
+            value: {account: account3, count: 1},
+          });
+        });
+
+        test('getSuggestions', done => {
+          provider.getSuggestions().then(reviewers => {
+            // Default is no filtering.
+            assert.equal(reviewers.length, 6);
+            assert.deepEqual(reviewers,
+                [redundantSuggestion1, redundantSuggestion2,
+                  redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
+          }).then(done);
+        });
+
+        test('getSuggestions short circuits when logged out', () => {
+          // API call is already stubbed.
+          const xhrSpy = restAPI.getChangeSuggestedReviewers;
+          provider._loggedIn = false;
+          return provider.getSuggestions('').then(() => {
+            assert.isFalse(xhrSpy.called);
+            provider._loggedIn = true;
+            return provider.getSuggestions('').then(() => {
+              assert.isTrue(xhrSpy.called);
+            });
+          });
+        });
+      });
+
+      test('getChangeSuggestedReviewers is used', done => {
+        const suggestReviewerStub =
+            sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
+                .returns(Promise.resolve([]));
+        const suggestAccountStub =
+            sandbox.stub(restAPI, 'getSuggestedAccounts')
+                .returns(Promise.resolve([]));
+
+        provider.getSuggestions('').then(() => {
+          assert.isTrue(suggestReviewerStub.calledOnce);
+          assert.isTrue(suggestReviewerStub.calledWith(42, ''));
+          assert.isFalse(suggestAccountStub.called);
+          done();
+        });
+      });
+    });
+
+    suite('allowAnyUser set to true', () => {
+      setup(done => {
+        provider = new GrReviewerSuggestionsProvider(restAPI, change._number,
+            true);
+        provider.init().then(done);
+      });
+
+      test('getSuggestedAccounts is used', done => {
+        const suggestReviewerStub =
+            sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
+                .returns(Promise.resolve([]));
+        const suggestAccountStub =
+            sandbox.stub(restAPI, 'getSuggestedAccounts')
+                .returns(Promise.resolve([]));
+
+        provider.getSuggestions('').then(() => {
+          assert.isFalse(suggestReviewerStub.called);
+          assert.isTrue(suggestAccountStub.calledOnce);
+          assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
+          done();
+        });
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/template_test_srcs/template_test.js b/polygerrit-ui/app/template_test_srcs/template_test.js
index ac0dbeb..ec3b7d5 100644
--- a/polygerrit-ui/app/template_test_srcs/template_test.js
+++ b/polygerrit-ui/app/template_test_srcs/template_test.js
@@ -38,6 +38,8 @@
   'SiteBasedCache',
   'FetchPromisesCache',
   'GrRestApiHelper',
+  'GrDisplayNameUtils',
+  'GrReviewerSuggestionsProvider',
   'moment',
   'page',
   'util',
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 9c06113..4754cd8 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -23,6 +23,7 @@
 <script src="/bower_components/web-component-tester/browser.js"></script>
 <script>
   const testFiles = [];
+  const scriptsPath = '../scripts/';
   const elementsPath = '../elements/';
   const behaviorsPath = '../behaviors/';
 
@@ -61,9 +62,9 @@
     'change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html',
     'change-list/gr-create-change-help/gr-create-change-help_test.html',
     'change-list/gr-dashboard-view/gr-dashboard-view_test.html',
+    // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+    // 'change-list/gr-repo-header/gr-repo-header_test.html',
     'change-list/gr-user-header/gr-user-header_test.html',
-    'change/gr-account-entry/gr-account-entry_test.html',
-    'change/gr-account-list/gr-account-list_test.html',
     'change/gr-change-actions/gr-change-actions_test.html',
     'change/gr-change-metadata/gr-change-metadata-it_test.html',
     'change/gr-change-metadata/gr-change-metadata_test.html',
@@ -105,10 +106,13 @@
     'core/gr-search-bar/gr-search-bar_test.html',
     'core/gr-smart-search/gr-smart-search_test.html',
     'diff/gr-comment-api/gr-comment-api_test.html',
+    'diff/gr-coverage-layer/gr-coverage-layer_test.html',
     'diff/gr-diff-builder/gr-diff-builder_test.html',
     'diff/gr-diff-cursor/gr-diff-cursor_test.html',
     'diff/gr-diff-highlight/gr-annotation_test.html',
     'diff/gr-diff-highlight/gr-diff-highlight_test.html',
+    // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+    // 'diff/gr-diff-host/gr-diff-host_test.html',
     'diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html',
     'diff/gr-diff-processor/gr-diff-processor_test.html',
     'diff/gr-diff-selection/gr-diff-selection_test.html',
@@ -127,6 +131,8 @@
     'plugins/gr-admin-api/gr-admin-api_test.html',
     'plugins/gr-styles-api/gr-styles-api_test.html',
     'plugins/gr-attribute-helper/gr-attribute-helper_test.html',
+    // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+    // 'plugins/gr-dom-hooks/gr-dom-hooks_test.html',
     'plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html',
     'plugins/gr-event-helper/gr-event-helper_test.html',
     'plugins/gr-external-style/gr-external-style_test.html',
@@ -135,7 +141,10 @@
     'plugins/gr-popup-interface/gr-popup-interface_test.html',
     'plugins/gr-repo-api/gr-repo-api_test.html',
     'plugins/gr-settings-api/gr-settings-api_test.html',
+    'plugins/gr-theme-api/gr-theme-api_test.html',
     'settings/gr-account-info/gr-account-info_test.html',
+    // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+    // 'settings/gr-agreements-list/gr-agreements-list_test.html',
     'settings/gr-change-table-editor/gr-change-table-editor_test.html',
     'settings/gr-cla-view/gr-cla-view_test.html',
     'settings/gr-edit-preferences/gr-edit-preferences_test.html',
@@ -149,7 +158,9 @@
     'settings/gr-settings-view/gr-settings-view_test.html',
     'settings/gr-ssh-editor/gr-ssh-editor_test.html',
     'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
+    'shared/gr-account-entry/gr-account-entry_test.html',
     'shared/gr-account-label/gr-account-label_test.html',
+    'shared/gr-account-list/gr-account-list_test.html',
     'shared/gr-account-link/gr-account-link_test.html',
     'shared/gr-alert/gr-alert_test.html',
     'shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html',
@@ -161,34 +172,48 @@
     'shared/gr-comment-thread/gr-comment-thread_test.html',
     'shared/gr-comment/gr-comment_test.html',
     'shared/gr-copy-clipboard/gr-copy-clipboard_test.html',
+    'shared/gr-count-string-formatter/gr-count-string-formatter_test.html',
     'shared/gr-cursor-manager/gr-cursor-manager_test.html',
     'shared/gr-date-formatter/gr-date-formatter_test.html',
     'shared/gr-dialog/gr-dialog_test.html',
     'shared/gr-diff-preferences/gr-diff-preferences_test.html',
     'shared/gr-download-commands/gr-download-commands_test.html',
+    'shared/gr-dropdown/gr-dropdown_test.html',
     'shared/gr-dropdown-list/gr-dropdown-list_test.html',
     'shared/gr-editable-content/gr-editable-content_test.html',
     'shared/gr-editable-label/gr-editable-label_test.html',
     'shared/gr-formatted-text/gr-formatted-text_test.html',
+    'shared/gr-hovercard/gr-hovercard_test.html',
+    'shared/gr-js-api-interface/gr-annotation-actions-context_test.html',
+    'shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html',
     'shared/gr-js-api-interface/gr-change-actions-js-api_test.html',
     'shared/gr-js-api-interface/gr-change-reply-js-api_test.html',
     'shared/gr-js-api-interface/gr-js-api-interface_test.html',
+    // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+    // 'shared/gr-js-api-interface/gr-plugin-action-context_test.html',
     'shared/gr-js-api-interface/gr-plugin-endpoints_test.html',
     'shared/gr-js-api-interface/gr-plugin-rest-api_test.html',
     'shared/gr-fixed-panel/gr-fixed-panel_test.html',
     'shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html',
+    // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+    // 'shared/gr-label-info/gr-label-info_test.html',
     'shared/gr-lib-loader/gr-lib-loader_test.html',
     'shared/gr-limited-text/gr-limited-text_test.html',
     'shared/gr-linked-chip/gr-linked-chip_test.html',
     'shared/gr-linked-text/gr-linked-text_test.html',
     'shared/gr-list-view/gr-list-view_test.html',
+    'shared/gr-overlay/gr-overlay_test.html',
     'shared/gr-page-nav/gr-page-nav_test.html',
     'shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html',
     'shared/gr-rest-api-interface/gr-auth_test.html',
+    'shared/gr-rest-api-interface/gr-etag-decorator_test.html',
     'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
     'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html',
     'shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html',
+    // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+    // 'shared/gr-rest-api-interface/mock-diff-response_test.html',
     'shared/gr-select/gr-select_test.html',
+    'shared/gr-shell-command/gr-shell-command_test.html',
     'shared/gr-storage/gr-storage_test.html',
     'shared/gr-textarea/gr-textarea_test.html',
     'shared/gr-tooltip-content/gr-tooltip-content_test.html',
@@ -212,8 +237,10 @@
     'rest-client-behavior/rest-client-behavior_test.html',
     'gr-access-behavior/gr-access-behavior_test.html',
     'gr-admin-nav-behavior/gr-admin-nav-behavior_test.html',
-    'gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html',
     'gr-change-table-behavior/gr-change-table-behavior_test.html',
+    // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+    // 'gr-list-view-behavior/gr-list-view-behavior_test.html',
+    'gr-display-name-behavior/gr-display-name-behavior_test.html',
     'gr-patch-set-behavior/gr-patch-set-behavior_test.html',
     'gr-path-list-behavior/gr-path-list-behavior_test.html',
     'gr-tooltip-behavior/gr-tooltip-behavior_test.html',
@@ -227,5 +254,17 @@
     testFiles.push(file);
   }
 
+  const scripts = [
+    'gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html',
+    'gr-group-suggestions-provider/gr-group-suggestions-provider_test.html',
+    'gr-display-name-utils/gr-display-name-utils_test.html',
+    'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
+  ];
+  /* eslint-enable max-len */
+  for (let file of scripts) {
+    file = scriptsPath + file;
+    testFiles.push(file);
+  }
+
   WCT.loadSuites(testFiles);
 </script>