Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static/js/tracker/tracker-ac.js b/static/js/tracker/tracker-ac.js
new file mode 100644
index 0000000..4d98ac1
--- /dev/null
+++ b/static/js/tracker/tracker-ac.js
@@ -0,0 +1,1285 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+/* eslint-disable camelcase */
+/* eslint-disable no-unused-vars */
+
+/**
+ * This file contains the autocomplete configuration logic that is
+ * specific to the issue fields of Monorail.  It depends on ac.js, our
+ * modified version of the autocomplete library.
+ */
+
+/**
+ * This is an autocomplete store that holds the hotlists of the current user.
+ */
+let TKR_hotlistsStore;
+
+/**
+ * This is an autocomplete store that holds well-known issue label
+ * values for the current project.
+ */
+let TKR_labelStore;
+
+/**
+ * Like TKR_labelStore but stores only label prefixes.
+ */
+let TKR_labelPrefixStore;
+
+/**
+ * Like TKR_labelStore but adds a trailing comma instead of replacing.
+ */
+let TKR_labelMultiStore;
+
+/**
+ * This is an autocomplete store that holds issue components.
+ */
+let TKR_componentStore;
+
+/**
+ * Like TKR_componentStore but adds a trailing comma instead of replacing.
+ */
+let TKR_componentListStore;
+
+/**
+ * This is an autocomplete store that holds many different kinds of
+ * items that can be shown in the artifact search autocomplete.
+ */
+let TKR_searchStore;
+
+/**
+ * This is similar to TKR_searchStore, but does not include any suggestions
+ * to use the "me" keyword. Using "me" is not a good idea for project canned
+ * queries and filter rules.
+ */
+let TKR_projectQueryStore;
+
+/**
+ * This is an autocomplete store that holds items for the quick edit
+ * autocomplete.
+ */
+// TODO(jrobbins): add options for fields and components.
+let TKR_quickEditStore;
+
+/**
+ * This is a list of label prefixes that each issue should only use once.
+ * E.g., each issue should only have one Priority-* label.  We do not prevent
+ * the user from using multiple such labels, we just warn the user before
+ * they submit.
+ */
+let TKR_exclPrefixes = [];
+
+/**
+ * This is an autocomplete store that holds custom permission names that
+ * have already been used in this project.
+ */
+let TKR_customPermissionsStore;
+
+
+/**
+ * This is an autocomplete store that holds well-known issue status
+ * values for the current project.
+ */
+let TKR_statusStore;
+
+
+/**
+ * This is an autocomplete store that holds the usernames of all the
+ * members of the current project.  This is used for autocomplete in
+ * the cc-list of an issue, where many user names can entered with
+ * commas between them.
+ */
+let TKR_memberListStore;
+
+
+/**
+ * This is an autocomplete store that holds the projects that the current
+ * user is contributor/member/owner of.
+ */
+let TKR_projectStore;
+
+/**
+ * This is an autocomplete store that holds the usernames of possible
+ * issue owners in the current project.  The list of possible issue
+ * owners is the same as the list of project members, but the behavior
+ * of this autocompete store is different because the issue owner text
+ * field can only accept one value.
+ */
+let TKR_ownerStore;
+
+
+/**
+ * This is an autocomplete store that holds any list of string for choices.
+ */
+let TKR_autoCompleteStore;
+
+
+/**
+ * An array of autocomplete stores used for user-type custom fields.
+ */
+const TKR_userAutocompleteStores = [];
+
+
+/**
+ * This boolean controls whether odd-ball status and labels are treated as
+ * a warning or an error.  Normally, it is False.
+ */
+// TODO(jrobbins): split this into one option for statuses and one for labels.
+let TKR_restrict_to_known;
+
+/**
+ * This substitute function should be used for multi-valued autocomplete fields
+ * that are delimited by commas. When we insert an autocomplete value, replace
+ * an entire search term. Add a comma and a space after it if it is a complete
+ * search term.
+ */
+function TKR_acSubstituteWithComma(inputValue, caret, completable, completion) {
+  let nextTerm = caret;
+
+  // Subtract one in case the cursor is at the end of the input, before a comma.
+  let prevTerm = caret - 1;
+  while (nextTerm < inputValue.length - 1 && inputValue.charAt(nextTerm) !== ',') {
+    nextTerm++;
+  }
+  // Set this at the position after the found comma.
+  nextTerm++;
+
+  while (prevTerm > 0 && ![',', ' '].includes(inputValue.charAt(prevTerm))) {
+    prevTerm--;
+  }
+  if (prevTerm > 0) {
+    // Set this boundary after the found space/comma if it's not the beginning
+    // of the field.
+    prevTerm++;
+  }
+
+  return inputValue.substring(0, prevTerm) +
+         completion.value + ', ' + inputValue.substring(nextTerm);
+}
+
+/**
+ * When the prefix starts with '*', return the complete set of all
+ * possible completions.
+ * @param {string} prefix If this starts with '*', return all possible
+ * completions.  Otherwise return null.
+ * @param {Array} labelDefs The array of label names and docstrings.
+ * @return Array of new _AC_Completions for each possible completion, or null.
+ */
+function TKR_fullComplete(prefix, labelDefs) {
+  if (!prefix.startsWith('*')) return null;
+  const out = [];
+  for (let i = 0; i < labelDefs.length; i++) {
+    out.push(new _AC_Completion(labelDefs[i].name,
+        labelDefs[i].name,
+        labelDefs[i].doc));
+  }
+  return out;
+}
+
+
+/**
+ * Constucts a list of all completions for both open and closed
+ * statuses, with a header for each group.
+ * @param {string} prefix If starts with '*', return all possible completions,
+ * else return null.
+ * @param {Array} openStatusDefs The array of open status values and
+ * docstrings.
+ * @param {Array} closedStatusDefs The array of closed status values
+ * and docstrings.
+ * @return Array of new _AC_Completions for each possible completion, or null.
+ */
+function TKR_openClosedComplete(prefix, openStatusDefs, closedStatusDefs) {
+  if (!prefix.startsWith('*')) return null;
+  const out = [];
+  out.push({heading: 'Open Statuses:'}); // TODO: i18n
+  for (var i = 0; i < openStatusDefs.length; i++) {
+    out.push(new _AC_Completion(openStatusDefs[i].name,
+        openStatusDefs[i].name,
+        openStatusDefs[i].doc));
+  }
+  out.push({heading: 'Closed Statuses:'}); // TODO: i18n
+  for (var i = 0; i < closedStatusDefs.length; i++) {
+    out.push(new _AC_Completion(closedStatusDefs[i].name,
+        closedStatusDefs[i].name,
+        closedStatusDefs[i].doc));
+  }
+  return out;
+}
+
+
+function TKR_setUpHotlistsStore(hotlists) {
+  const docdict = {};
+  const ref_strs = [];
+
+  for (let i = 0; i < hotlists.length; i++) {
+    ref_strs.push(hotlists[i]['ref_str']);
+    docdict[hotlists[i]['ref_str']] = hotlists[i]['summary'];
+  }
+
+  TKR_hotlistsStore = new _AC_SimpleStore(ref_strs, docdict);
+  TKR_hotlistsStore.substitute = TKR_acSubstituteWithComma;
+}
+
+
+/**
+ * An array of definitions of all well-known issue statuses.  Each
+ * definition has the name of the status value, and a docstring that
+ * describes its meaning.
+ */
+let TKR_statusWords = [];
+
+
+/**
+ * Constuct a new autocomplete store with all the well-known issue
+ * status values.  The store has some DIT-specific methods.
+ * TODO(jrobbins): would it be easier to define my own class to use
+ * instead of _AC_Simple_Store?
+ * @param {Array} openStatusDefs An array of definitions of the
+ * well-known open status values.  Each definition has a name and
+ * docstring.
+ * @param {Array} closedStatusDefs An array of definitions of the
+ * well-known closed status values.  Each definition has a name and
+ * docstring.
+ */
+function TKR_setUpStatusStore(openStatusDefs, closedStatusDefs) {
+  const docdict = {};
+  TKR_statusWords = [];
+  for (var i = 0; i < openStatusDefs.length; i++) {
+    var status = openStatusDefs[i];
+    TKR_statusWords.push(status.name);
+    docdict[status.name] = status.doc;
+  }
+  for (var i = 0; i < closedStatusDefs.length; i++) {
+    var status = closedStatusDefs[i];
+    TKR_statusWords.push(status.name);
+    docdict[status.name] = status.doc;
+  }
+
+  TKR_statusStore = new _AC_SimpleStore(TKR_statusWords, docdict);
+
+  TKR_statusStore.commaCompletes = false;
+
+  TKR_statusStore.substitute =
+  function(inputValue, cursor, completable, completion) {
+    return completion.value;
+  };
+
+  TKR_statusStore.completable = function(inputValue, cursor) {
+    if (!ac_everTyped) return '*status';
+    return inputValue;
+  };
+
+  TKR_statusStore.completions = function(prefix, tofilter) {
+    const fullList = TKR_openClosedComplete(prefix,
+        openStatusDefs,
+        closedStatusDefs);
+    if (fullList) return fullList;
+    return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
+  };
+}
+
+
+/**
+ * Simple function to add a given item to the list of items used to construct
+ * an "autocomplete store", and also update the docstring that describes
+ * that item.  They are stored separately for backward compatability with
+ * autocomplete store logic that preceeded the introduction of descriptions.
+ */
+function TKR_addACItem(items, docDict, item, docStr) {
+  items.push(item);
+  docDict[item] = docStr;
+}
+
+/**
+ * Adds a group of three items related to a date field.
+ */
+function TKR_addACDateItems(items, docDict, fieldName, humanReadable) {
+  const today = new Date();
+  const todayStr = (today.getFullYear() + '-' + (today.getMonth() + 1) + '-' +
+    today.getDate());
+  TKR_addACItem(items, docDict, fieldName + '>today-1',
+      humanReadable + ' within the last N days');
+  TKR_addACItem(items, docDict, fieldName + '>' + todayStr,
+      humanReadable + ' after the specified date');
+  TKR_addACItem(items, docDict, fieldName + '<today-1',
+      humanReadable + ' more than N days ago');
+}
+
+/**
+ * Add several autocomplete items to a word list that will be used to construct
+ * an autocomplete store.  Also, keep track of description strings for each
+ * item.  A search operator is prepended to the name of each item.  The opt_old
+ * and opt_new parameters are used to transform Key-Value labels into Key=Value
+ * search terms.
+ */
+function TKR_addACItemList(
+    items, docDict, searchOp, acDefs, opt_old, opt_new) {
+  let item;
+  for (let i = 0; i < acDefs.length; i++) {
+    const nameAndDoc = acDefs[i];
+    item = searchOp + nameAndDoc.name;
+    if (opt_old) {
+      // Preserve any leading minus-sign.
+      item = item.slice(0, 1) + item.slice(1).replace(opt_old, opt_new);
+    }
+    TKR_addACItem(items, docDict, item, nameAndDoc.doc);
+  }
+}
+
+
+/**
+ * Use information from an options feed to populate the artifact search
+ * autocomplete menu.  The order of sections is: custom fields, labels,
+ * components, people, status, special, dates.  Within each section,
+ * options are ordered semantically where possible, or alphabetically
+ * if there is no semantic ordering.  Negated options all come after
+ * all normal options.
+ */
+function TKR_setUpSearchStore(
+    labelDefs, memberDefs, openDefs, closedDefs, componentDefs, fieldDefs,
+    indMemberDefs) {
+  let searchWords = [];
+  const searchWordsNeg = [];
+  const docDict = {};
+
+  // Treat Key-Value and OneWord labels separately.
+  const keyValueLabelDefs = [];
+  const oneWordLabelDefs = [];
+  for (var i = 0; i < labelDefs.length; i++) {
+    const nameAndDoc = labelDefs[i];
+    if (nameAndDoc.name.indexOf('-') == -1) {
+      oneWordLabelDefs.push(nameAndDoc);
+    } else {
+      keyValueLabelDefs.push(nameAndDoc);
+    }
+  }
+
+  // Autocomplete for custom fields.
+  for (i = 0; i < fieldDefs.length; i++) {
+    const fieldName = fieldDefs[i]['field_name'];
+    const fieldType = fieldDefs[i]['field_type'];
+    if (fieldType == 'ENUM_TYPE') {
+      const choices = fieldDefs[i]['choices'];
+      TKR_addACItemList(searchWords, docDict, fieldName + '=', choices);
+      TKR_addACItemList(searchWordsNeg, docDict, '-' + fieldName + '=', choices);
+    } else if (fieldType == 'STR_TYPE') {
+      TKR_addACItem(searchWords, docDict, fieldName + ':',
+          fieldDefs[i]['docstring']);
+    } else if (fieldType == 'DATE_TYPE') {
+      TKR_addACItem(searchWords, docDict, fieldName + ':',
+          fieldDefs[i]['docstring']);
+      TKR_addACDateItems(searchWords, docDict, fieldName, fieldName);
+    } else {
+      TKR_addACItem(searchWords, docDict, fieldName + '=',
+          fieldDefs[i]['docstring']);
+    }
+    TKR_addACItem(searchWords, docDict, 'has:' + fieldName,
+        'Issues with any ' + fieldName + ' value');
+    TKR_addACItem(searchWordsNeg, docDict, '-has:' + fieldName,
+        'Issues with no ' + fieldName + ' value');
+  }
+
+  // Add suggestions with "me" first, because otherwise they may be impossible
+  // to reach in a project that has a lot of members with emails starting with
+  // "me".
+  if (CS_env['loggedInUserEmail']) {
+    TKR_addACItem(searchWords, docDict, 'owner:me', 'Issues owned by me');
+    TKR_addACItem(searchWordsNeg, docDict, '-owner:me', 'Issues not owned by me');
+    TKR_addACItem(searchWords, docDict, 'cc:me', 'Issues that CC me');
+    TKR_addACItem(searchWordsNeg, docDict, '-cc:me', 'Issues that don\'t CC me');
+    TKR_addACItem(searchWords, docDict, 'reporter:me', 'Issues I reported');
+    TKR_addACItem(searchWordsNeg, docDict, '-reporter:me', 'Issues reported by others');
+    TKR_addACItem(searchWords, docDict, 'commentby:me',
+        'Issues that I commented on');
+    TKR_addACItem(searchWordsNeg, docDict, '-commentby:me',
+        'Issues that I didn\'t comment on');
+  }
+
+  TKR_addACItemList(searchWords, docDict, '', keyValueLabelDefs, '-', '=');
+  TKR_addACItemList(searchWordsNeg, docDict, '-', keyValueLabelDefs, '-', '=');
+  TKR_addACItemList(searchWords, docDict, 'label:', oneWordLabelDefs);
+  TKR_addACItemList(searchWordsNeg, docDict, '-label:', oneWordLabelDefs);
+
+  TKR_addACItemList(searchWords, docDict, 'component:', componentDefs);
+  TKR_addACItemList(searchWordsNeg, docDict, '-component:', componentDefs);
+  TKR_addACItem(searchWords, docDict, 'has:component',
+      'Issues with any components specified');
+  TKR_addACItem(searchWordsNeg, docDict, '-has:component',
+      'Issues with no components specified');
+
+  TKR_addACItemList(searchWords, docDict, 'owner:', indMemberDefs);
+  TKR_addACItemList(searchWordsNeg, docDict, '-owner:', indMemberDefs);
+  TKR_addACItemList(searchWords, docDict, 'cc:', memberDefs);
+  TKR_addACItemList(searchWordsNeg, docDict, '-cc:', memberDefs);
+  TKR_addACItem(searchWords, docDict, 'has:cc',
+      'Issues with any cc\'d users');
+  TKR_addACItem(searchWordsNeg, docDict, '-has:cc',
+      'Issues with no cc\'d users');
+  TKR_addACItemList(searchWords, docDict, 'reporter:', memberDefs);
+  TKR_addACItemList(searchWordsNeg, docDict, '-reporter:', memberDefs);
+  TKR_addACItemList(searchWords, docDict, 'status:', openDefs);
+  TKR_addACItemList(searchWordsNeg, docDict, '-status:', openDefs);
+  TKR_addACItemList(searchWords, docDict, 'status:', closedDefs);
+  TKR_addACItemList(searchWordsNeg, docDict, '-status:', closedDefs);
+  TKR_addACItem(searchWords, docDict, 'has:status',
+      'Issues with any status');
+  TKR_addACItem(searchWordsNeg, docDict, '-has:status',
+      'Issues with no status');
+
+  TKR_addACItem(searchWords, docDict, 'is:blocked',
+      'Issues that are blocked');
+  TKR_addACItem(searchWordsNeg, docDict, '-is:blocked',
+      'Issues that are not blocked');
+  TKR_addACItem(searchWords, docDict, 'has:blockedon',
+      'Issues that are blocked');
+  TKR_addACItem(searchWordsNeg, docDict, '-has:blockedon',
+      'Issues that are not blocked');
+  TKR_addACItem(searchWords, docDict, 'has:blocking',
+      'Issues that are blocking other issues');
+  TKR_addACItem(searchWordsNeg, docDict, '-has:blocking',
+      'Issues that are not blocking other issues');
+  TKR_addACItem(searchWords, docDict, 'has:mergedinto',
+      'Issues that were merged into other issues');
+  TKR_addACItem(searchWordsNeg, docDict, '-has:mergedinto',
+      'Issues that were not merged into other issues');
+
+  TKR_addACItem(searchWords, docDict, 'is:starred',
+      'Starred by me');
+  TKR_addACItem(searchWordsNeg, docDict, '-is:starred',
+      'Not starred by me');
+  TKR_addACItem(searchWords, docDict, 'stars>10',
+      'More than 10 stars');
+  TKR_addACItem(searchWords, docDict, 'stars>100',
+      'More than 100 stars');
+  TKR_addACItem(searchWords, docDict, 'summary:',
+      'Search within the summary field');
+
+  TKR_addACItemList(searchWords, docDict, 'commentby:', memberDefs);
+  TKR_addACItem(searchWords, docDict, 'attachment:',
+      'Search within attachment names');
+  TKR_addACItem(searchWords, docDict, 'attachments>5',
+      'Has more than 5 attachments');
+  TKR_addACItem(searchWords, docDict, 'is:open', 'Issues that are open');
+  TKR_addACItem(searchWordsNeg, docDict, '-is:open', 'Issues that are closed');
+  TKR_addACItem(searchWords, docDict, 'has:owner',
+      'Issues with some owner');
+  TKR_addACItem(searchWordsNeg, docDict, '-has:owner',
+      'Issues with no owner');
+  TKR_addACItem(searchWords, docDict, 'has:attachments',
+      'Issues with some attachments');
+  TKR_addACItem(searchWords, docDict, 'id:1,2,3',
+      'Match only the specified issues');
+  TKR_addACItem(searchWords, docDict, 'id<100000',
+      'Issues with IDs under 100,000');
+  TKR_addACItem(searchWords, docDict, 'blockedon:1',
+      'Blocked on the specified issues');
+  TKR_addACItem(searchWords, docDict, 'blocking:1',
+      'Blocking the specified issues');
+  TKR_addACItem(searchWords, docDict, 'mergedinto:1',
+      'Merged into the specified issues');
+  TKR_addACItem(searchWords, docDict, 'is:ownerbouncing',
+      'Issues with owners we cannot contact');
+  TKR_addACItem(searchWords, docDict, 'is:spam', 'Issues classified as spam');
+  // We do not suggest -is:spam because it is implicit.
+
+  TKR_addACDateItems(searchWords, docDict, 'opened', 'Opened');
+  TKR_addACDateItems(searchWords, docDict, 'modified', 'Modified');
+  TKR_addACDateItems(searchWords, docDict, 'closed', 'Closed');
+  TKR_addACDateItems(searchWords, docDict, 'ownermodified', 'Owner field modified');
+  TKR_addACDateItems(searchWords, docDict, 'ownerlastvisit', 'Owner last visit');
+  TKR_addACDateItems(searchWords, docDict, 'statusmodified', 'Status field modified');
+  TKR_addACDateItems(
+      searchWords, docDict, 'componentmodified', 'Component field modified');
+
+  TKR_projectQueryStore = new _AC_SimpleStore(searchWords, docDict);
+
+  searchWords = searchWords.concat(searchWordsNeg);
+
+  TKR_searchStore = new _AC_SimpleStore(searchWords, docDict);
+
+  // When we insert an autocomplete value, replace an entire search term.
+  // Add just a space after it (not a comma) if it is a complete search term,
+  // or leave the caret immediately after the completion if we are just helping
+  // the user with the search operator.
+  TKR_searchStore.substitute =
+      function(inputValue, caret, completable, completion) {
+        let nextTerm = caret;
+        while (inputValue.charAt(nextTerm) != ' ' &&
+               nextTerm < inputValue.length) {
+          nextTerm++;
+        }
+        while (inputValue.charAt(nextTerm) == ' ' &&
+               nextTerm < inputValue.length) {
+          nextTerm++;
+        }
+        return inputValue.substring(0, caret - completable.length) +
+               completion.value + ' ' + inputValue.substring(nextTerm);
+      };
+  TKR_searchStore.autoselectFirstRow =
+      function() {
+        return false;
+      };
+
+  TKR_projectQueryStore.substitute = TKR_searchStore.substitute;
+  TKR_projectQueryStore.autoselectFirstRow = TKR_searchStore.autoselectFirstRow;
+}
+
+
+/**
+ * Use information from an options feed to populate the issue quick edit
+ * autocomplete menu.
+ */
+function TKR_setUpQuickEditStore(
+    labelDefs, memberDefs, openDefs, closedDefs, indMemberDefs) {
+  const qeWords = [];
+  const docDict = {};
+
+  // Treat Key-Value and OneWord labels separately.
+  const keyValueLabelDefs = [];
+  const oneWordLabelDefs = [];
+  for (let i = 0; i < labelDefs.length; i++) {
+    const nameAndDoc = labelDefs[i];
+    if (nameAndDoc.name.indexOf('-') == -1) {
+      oneWordLabelDefs.push(nameAndDoc);
+    } else {
+      keyValueLabelDefs.push(nameAndDoc);
+    }
+  }
+  TKR_addACItemList(qeWords, docDict, '', keyValueLabelDefs, '-', '=');
+  TKR_addACItemList(qeWords, docDict, '-', keyValueLabelDefs, '-', '=');
+  TKR_addACItemList(qeWords, docDict, '', oneWordLabelDefs);
+  TKR_addACItemList(qeWords, docDict, '-', oneWordLabelDefs);
+
+  TKR_addACItem(qeWords, docDict, 'owner=me', 'Make me the owner');
+  TKR_addACItem(qeWords, docDict, 'owner=----', 'Clear the owner field');
+  TKR_addACItem(qeWords, docDict, 'cc=me', 'CC me on this issue');
+  TKR_addACItem(qeWords, docDict, 'cc=-me', 'Remove me from CC list');
+  TKR_addACItemList(qeWords, docDict, 'owner=', indMemberDefs);
+  TKR_addACItemList(qeWords, docDict, 'cc=', memberDefs);
+  TKR_addACItemList(qeWords, docDict, 'cc=-', memberDefs);
+  TKR_addACItemList(qeWords, docDict, 'status=', openDefs);
+  TKR_addACItemList(qeWords, docDict, 'status=', closedDefs);
+  TKR_addACItem(qeWords, docDict, 'summary=""', 'Set the summary field');
+
+  TKR_quickEditStore = new _AC_SimpleStore(qeWords, docDict);
+
+  // When we insert an autocomplete value, replace an entire command part.
+  // Add just a space after it (not a comma) if it is a complete part,
+  // or leave the caret immediately after the completion if we are just helping
+  // the user with the command operator.
+  TKR_quickEditStore.substitute =
+      function(inputValue, caret, completable, completion) {
+        let nextTerm = caret;
+        while (inputValue.charAt(nextTerm) != ' ' &&
+               nextTerm < inputValue.length) {
+          nextTerm++;
+        }
+        while (inputValue.charAt(nextTerm) == ' ' &&
+               nextTerm < inputValue.length) {
+          nextTerm++;
+        }
+        return inputValue.substring(0, caret - completable.length) +
+               completion.value + ' ' + inputValue.substring(nextTerm);
+      };
+}
+
+
+/**
+ * Constuct a new autocomplete store with all the project
+ * custom permissions.
+ * @param {Array} customPermissions An array of custom permission names.
+ */
+function TKR_setUpCustomPermissionsStore(customPermissions) {
+  customPermissions = customPermissions || [];
+  const permWords = ['View', 'EditIssue', 'AddIssueComment', 'DeleteIssue'];
+  const docdict = {
+    'View': '', 'EditIssue': '', 'AddIssueComment': '', 'DeleteIssue': ''};
+  for (let i = 0; i < customPermissions.length; i++) {
+    permWords.push(customPermissions[i]);
+    docdict[customPermissions[i]] = '';
+  }
+
+  TKR_customPermissionsStore = new _AC_SimpleStore(permWords, docdict);
+
+  TKR_customPermissionsStore.commaCompletes = false;
+
+  TKR_customPermissionsStore.substitute =
+  function(inputValue, cursor, completable, completion) {
+    return completion.value;
+  };
+}
+
+
+/**
+ * Constuct a new autocomplete store with all the well-known project
+ * member user names and real names.  The store has some
+ * monorail-specific methods.
+ * TODO(jrobbins): would it be easier to define my own class to use
+ * instead of _AC_Simple_Store?
+ * @param {Array} memberDefs an array of member objects.
+ * @param {Array} nonGroupMemberDefs an array of member objects who are not groups.
+ */
+function TKR_setUpMemberStore(memberDefs, nonGroupMemberDefs) {
+  const memberWords = [];
+  const indMemberWords = [];
+  const docdict = {};
+
+  memberDefs.forEach((memberDef) => {
+    memberWords.push(memberDef.name);
+    docdict[memberDef.name] = null;
+  });
+  nonGroupMemberDefs.forEach((memberDef) => {
+    indMemberWords.push(memberDef.name);
+  });
+
+  TKR_memberListStore = new _AC_SimpleStore(memberWords, docdict);
+
+  TKR_memberListStore.completions = function(prefix, tofilter) {
+    const fullList = TKR_fullComplete(prefix, memberDefs);
+    if (fullList) return fullList;
+    return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
+  };
+
+  TKR_memberListStore.completable = function(inputValue, cursor) {
+    if (inputValue == '') return '*member';
+    return _AC_SimpleStore.prototype.completable.call(this, inputValue, cursor);
+  };
+
+  TKR_memberListStore.substitute = TKR_acSubstituteWithComma;
+
+  TKR_ownerStore = new _AC_SimpleStore(indMemberWords, docdict);
+
+  TKR_ownerStore.commaCompletes = false;
+
+  TKR_ownerStore.substitute =
+  function(inputValue, cursor, completable, completion) {
+    return completion.value;
+  };
+
+  TKR_ownerStore.completions = function(prefix, tofilter) {
+    const fullList = TKR_fullComplete(prefix, nonGroupMemberDefs);
+    if (fullList) return fullList;
+    return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
+  };
+
+  TKR_ownerStore.completable = function(inputValue, cursor) {
+    if (!ac_everTyped) return '*owner';
+    return inputValue;
+  };
+}
+
+
+/**
+ * Constuct one new autocomplete store for each user-valued custom
+ * field that has a needs_perm validation requirement, and thus a
+ * list of allowed user indexes.
+ * TODO(jrobbins): would it be easier to define my own class to use
+ * instead of _AC_Simple_Store?
+ * @param {Array} fieldDefs An array of field definitions, only some
+ * of which have a 'user_indexes' entry.
+ */
+function TKR_setUpUserAutocompleteStores(fieldDefs) {
+  fieldDefs.forEach((fieldDef) => {
+    if (fieldDef.qualifiedMembers) {
+      const us = makeOneUserAutocompleteStore(fieldDef);
+      TKR_userAutocompleteStores['custom_' + fieldDef['field_id']] = us;
+    }
+  });
+}
+
+function makeOneUserAutocompleteStore(fieldDef) {
+  const memberWords = [];
+  const docdict = {};
+  for (const member of fieldDef.qualifiedMembers) {
+    memberWords.push(member.name);
+    docdict[member.name] = member.doc;
+  }
+
+  const userStore = new _AC_SimpleStore(memberWords, docdict);
+  userStore.commaCompletes = false;
+
+  userStore.substitute =
+  function(inputValue, cursor, completable, completion) {
+    return completion.value;
+  };
+
+  userStore.completions = function(prefix, tofilter) {
+    const fullList = TKR_fullComplete(prefix, fieldDef.qualifiedMembers);
+    if (fullList) return fullList;
+    return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
+  };
+
+  userStore.completable = function(inputValue, cursor) {
+    if (!ac_everTyped) return '*custom';
+    return inputValue;
+  };
+
+  return userStore;
+}
+
+
+/**
+ * Constuct a new autocomplete store with all the components.
+ * The store has some monorail-specific methods.
+ * @param {Array} componentDefs An array of definitions of components.
+ */
+function TKR_setUpComponentStore(componentDefs) {
+  const componentWords = [];
+  const docdict = {};
+  for (let i = 0; i < componentDefs.length; i++) {
+    const component = componentDefs[i];
+    componentWords.push(component.name);
+    docdict[component.name] = component.doc;
+  }
+
+  const completions = function(prefix, tofilter) {
+    const fullList = TKR_fullComplete(prefix, componentDefs);
+    if (fullList) return fullList;
+    return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
+  };
+  const completable = function(inputValue, cursor) {
+    if (inputValue == '') return '*component';
+    return _AC_SimpleStore.prototype.completable.call(this, inputValue, cursor);
+  };
+
+  TKR_componentStore = new _AC_SimpleStore(componentWords, docdict);
+  TKR_componentStore.commaCompletes = false;
+  TKR_componentStore.substitute =
+  function(inputValue, cursor, completable, completion) {
+    return completion.value;
+  };
+  TKR_componentStore.completions = completions;
+  TKR_componentStore.completable = completable;
+
+  TKR_componentListStore = new _AC_SimpleStore(componentWords, docdict);
+  TKR_componentListStore.commaCompletes = false;
+  TKR_componentListStore.substitute = TKR_acSubstituteWithComma;
+  TKR_componentListStore.completions = completions;
+  TKR_componentListStore.completable = completable;
+}
+
+
+/**
+ * An array of definitions of all well-known issue labels.  Each
+ * definition has the name of the label, and a docstring that
+ * describes its meaning.
+ */
+let TKR_labelWords = [];
+
+
+/**
+ * Constuct a new autocomplete store with all the well-known issue
+ * labels for the current project.  The store has some DIT-specific methods.
+ * TODO(jrobbins): would it be easier to define my own class to use
+ * instead of _AC_Simple_Store?
+ * @param {Array} labelDefs An array of definitions of the project
+ * members.  Each definition has a name and docstring.
+ */
+function TKR_setUpLabelStore(labelDefs) {
+  TKR_labelWords = [];
+  const TKR_labelPrefixes = [];
+  const labelPrefs = new Set();
+  const docdict = {};
+  for (let i = 0; i < labelDefs.length; i++) {
+    const label = labelDefs[i];
+    TKR_labelWords.push(label.name);
+    TKR_labelPrefixes.push(label.name.split('-')[0]);
+    docdict[label.name] = label.doc;
+    labelPrefs.add(label.name.split('-')[0]);
+  }
+  const labelPrefArray = Array.from(labelPrefs);
+  const labelPrefDefs = labelPrefArray.map((s) => ({name: s, doc: ''}));
+
+  TKR_labelStore = new _AC_SimpleStore(TKR_labelWords, docdict);
+
+  TKR_labelStore.commaCompletes = false;
+  TKR_labelStore.substitute =
+  function(inputValue, cursor, completable, completion) {
+    return completion.value;
+  };
+
+  TKR_labelPrefixStore = new _AC_SimpleStore(TKR_labelPrefixes);
+
+  TKR_labelPrefixStore.commaCompletes = false;
+  TKR_labelPrefixStore.substitute =
+  function(inputValue, cursor, completable, completion) {
+    return completion.value;
+  };
+
+  TKR_labelMultiStore = new _AC_SimpleStore(TKR_labelWords, docdict);
+
+  TKR_labelMultiStore.substitute = TKR_acSubstituteWithComma;
+
+  const completable = function(inputValue, cursor) {
+    if (cursor === 0) {
+      return '*label'; // Show every well-known label that is not redundant.
+    }
+    let start = 0;
+    for (let i = cursor; --i >= 0;) {
+      const c = inputValue.charAt(i);
+      if (c === ' ' || c === ',') {
+        start = i + 1;
+        break;
+      }
+    }
+    const questionPos = inputValue.indexOf('?');
+    if (questionPos >= 0) {
+      // Ignore any "?" character and anything after it.
+      inputValue = inputValue.substring(start, questionPos);
+    }
+    let result = inputValue.substring(start, cursor);
+    if (inputValue.lastIndexOf('-') > 0 && !ac_everTyped) {
+      // Act like a menu: offer all alternative values for the same prefix.
+      result = inputValue.substring(
+          start, Math.min(cursor, inputValue.lastIndexOf('-')));
+    }
+    if (inputValue.startsWith('Restrict-') && !ac_everTyped) {
+      // If user is in the middle of 2nd part, use that to narrow the choices.
+      result = inputValue;
+      // If they completed 2nd part, give all choices matching 2-part prefix.
+      if (inputValue.lastIndexOf('-') > 8) {
+        result = inputValue.substring(
+            start, Math.min(cursor, inputValue.lastIndexOf('-') + 1));
+      }
+    }
+
+    return result;
+  };
+
+  const computeAvoid = function() {
+    const labelTextFields = Array.from(
+        document.querySelectorAll('.labelinput'));
+    const otherTextFields = labelTextFields.filter(
+        (tf) => (tf !== ac_focusedInput && tf.value));
+    return otherTextFields.map((tf) => tf.value);
+  };
+
+
+  const completions = function(labeldic) {
+    return function(prefix, tofilter) {
+      let comps = TKR_fullComplete(prefix, labeldic);
+      if (comps === null) {
+        comps = _AC_SimpleStore.prototype.completions.call(
+            this, prefix, tofilter);
+      }
+
+      const filteredComps = [];
+      for (const completion of comps) {
+        const completionLower = completion.value.toLowerCase();
+        const labelPrefix = completionLower.split('-')[0];
+        let alreadyUsed = false;
+        const isExclusive = FindInArray(TKR_exclPrefixes, labelPrefix) !== -1;
+        if (isExclusive) {
+          for (const usedLabel of ac_avoidValues) {
+            if (usedLabel.startsWith(labelPrefix + '-')) {
+              alreadyUsed = true;
+              break;
+            }
+          }
+        }
+        if (!alreadyUsed) {
+          filteredComps.push(completion);
+        }
+      }
+
+      return filteredComps;
+    };
+  };
+
+  TKR_labelStore.computeAvoid = computeAvoid;
+  TKR_labelStore.completable = completable;
+  TKR_labelStore.completions = completions(labelDefs);
+
+  TKR_labelPrefixStore.completable = completable;
+  TKR_labelPrefixStore.completions = completions(labelPrefDefs);
+
+  TKR_labelMultiStore.completable = completable;
+  TKR_labelMultiStore.completions = completions(labelDefs);
+}
+
+
+/**
+ * Constuct a new autocomplete store with the given strings as choices.
+ * @param {Array} choices An array of autocomplete choices.
+ */
+function TKR_setUpAutoCompleteStore(choices) {
+  TKR_autoCompleteStore = new _AC_SimpleStore(choices);
+  const choicesDefs = [];
+  for (let i = 0; i < choices.length; ++i) {
+    choicesDefs.push({'name': choices[i], 'doc': ''});
+  }
+
+  /**
+   * Override the default completions() function to return a list of
+   * available choices.  It proactively shows all choices when the user has
+   * not yet typed anything.  It stops offering choices if the text field
+   * has a pretty long string in it already.  It does not offer choices that
+   * have already been chosen.
+   */
+  TKR_autoCompleteStore.completions = function(prefix, tofilter) {
+    if (prefix.length > 18) {
+      return [];
+    }
+    let comps = TKR_fullComplete(prefix, choicesDefs);
+    if (comps == null) {
+      comps = _AC_SimpleStore.prototype.completions.call(
+          this, prefix, tofilter);
+    }
+
+    const usedComps = {};
+    const textFields = document.getElementsByTagName('input');
+    for (var i = 0; i < textFields.length; ++i) {
+      if (textFields[i].classList.contains('autocomplete')) {
+        usedComps[textFields[i].value] = true;
+      }
+    }
+    const unusedComps = [];
+    for (i = 0; i < comps.length; ++i) {
+      if (!usedComps[comps[i].value]) {
+        unusedComps.push(comps[i]);
+      }
+    }
+
+    return unusedComps;
+  };
+
+  /**
+   * Override the default completable() function with one that gives a
+   * special value when the user has not yet typed anything.  This
+   * causes TKR_fullComplete() to show all choices.  Also, always consider
+   * the whole textfield value as an input to completion matching.  Otherwise,
+   * it would only consider the part after the last comma (which makes sense
+   * for gmail To: and Cc: address fields).
+   */
+  TKR_autoCompleteStore.completable = function(inputValue, cursor) {
+    if (inputValue == '') {
+      return '*ac';
+    }
+    return inputValue;
+  };
+
+  /**
+   * Override the default substitute() function to completely replace the
+   * contents of the text field when the user selects a completion. Otherwise,
+   * it would append, much like the Gmail To: and Cc: fields append autocomplete
+   * selections.
+   */
+  TKR_autoCompleteStore.substitute =
+  function(inputValue, cursor, completable, completion) {
+    return completion.value;
+  };
+
+  /**
+   * We consider the whole textfield to be one value, not a comma separated
+   * list.  So, typing a ',' should not trigger an autocomplete selection.
+   */
+  TKR_autoCompleteStore.commaCompletes = false;
+}
+
+
+/**
+ * XMLHTTP object used to fetch autocomplete options from the server.
+ */
+const TKR_optionsXmlHttp = undefined;
+
+/**
+ * Contact the server to fetch the set of autocomplete options for the
+ * projects the user is contributor/member/owner of.
+ * @param {multiValue} boolean If set to true, the projectStore is configured to
+ * have support for multi-values (useful for example for saved queries where
+ * a query can apply to multiple projects).
+ */
+function TKR_fetchUserProjects(multiValue) {
+  // Set a request token to prevent XSRF leaking of user project lists.
+  const userRefs = [{displayName: window.CS_env.loggedInUserEmail}];
+  const userProjectsPromise = window.prpcClient.call(
+      'monorail.Users', 'GetUsersProjects', {userRefs});
+  userProjectsPromise.then((response) => {
+    const userProjects = response.usersProjects[0];
+    const projects = (userProjects.ownerOf || [])
+        .concat(userProjects.memberOf || [])
+        .concat(userProjects.contributorTo || []);
+    projects.sort();
+    if (projects) {
+      TKR_setUpProjectStore(projects, multiValue);
+    }
+  });
+}
+
+
+/**
+ * Constuct a new autocomplete store with all the projects that the
+ * current user has visibility into. The store has some monorail-specific
+ * methods.
+ * @param {Array} projects An array of project names.
+ * @param {boolean} multiValue Determines whether the store should support
+ *                  multiple values.
+ */
+function TKR_setUpProjectStore(projects, multiValue) {
+  const projectsDefs = [];
+  const docdict = {};
+  for (let i = 0; i < projects.length; ++i) {
+    projectsDefs.push({'name': projects[i], 'doc': ''});
+    docdict[projects[i]] = '';
+  }
+
+  TKR_projectStore = new _AC_SimpleStore(projects, docdict);
+  TKR_projectStore.commaCompletes = !multiValue;
+
+  if (multiValue) {
+    TKR_projectStore.substitute = TKR_acSubstituteWithComma;
+  } else {
+    TKR_projectStore.substitute =
+      function(inputValue, cursor, completable, completion) {
+        return completion.value;
+      };
+  }
+
+  TKR_projectStore.completions = function(prefix, tofilter) {
+    const fullList = TKR_fullComplete(prefix, projectsDefs);
+    if (fullList) return fullList;
+    return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
+  };
+
+  TKR_projectStore.completable = function(inputValue, cursor) {
+    if (inputValue == '') return '*project';
+    if (multiValue) {
+      return _AC_SimpleStore.prototype.completable.call(
+          this, inputValue, cursor);
+    } else {
+      return inputValue;
+    }
+  };
+}
+
+
+/**
+ * Convert the object resulting of a monorail.Projects ListStatuses to
+ * the format expected by TKR_populateAutocomplete.
+ * @param {object} statusesResponse A pRPC ListStatusesResponse object.
+ */
+function TKR_convertStatuses(statusesResponse) {
+  const statusDefs = statusesResponse.statusDefs || [];
+  const jsonData = {};
+
+  // Split statusDefs into open and closed name-doc objects.
+  jsonData.open = [];
+  jsonData.closed = [];
+  for (const s of statusDefs) {
+    if (!s.deprecated) {
+      const item = {
+        name: s.status,
+        doc: s.docstring,
+      };
+      if (s.meansOpen) {
+        jsonData.open.push(item);
+      } else {
+        jsonData.closed.push(item);
+      }
+    }
+  }
+
+  jsonData.strict = statusesResponse.restrictToKnown;
+
+  return jsonData;
+}
+
+
+/**
+ * Convert the object resulting of a monorail.Projects ListComponents to
+ * the format expected by TKR_populateAutocomplete.
+ * @param {object} componentsResponse A pRPC ListComponentsResponse object.
+ */
+function TKR_convertComponents(componentsResponse) {
+  const componentDefs = (componentsResponse.componentDefs || []);
+  const jsonData = {};
+
+  // Filter out deprecated components and normalize to name-doc object.
+  jsonData.components = [];
+  for (const c of componentDefs) {
+    if (!c.deprecated) {
+      jsonData.components.push({
+        name: c.path,
+        doc: c.docstring,
+      });
+    }
+  }
+
+  return jsonData;
+}
+
+
+/**
+ * Convert the object resulting of a monorail.Projects GetLabelOptions
+ * call to the format expected by TKR_populateAutocomplete.
+ * @param {object} labelsResponse A pRPC GetLabelOptionsResponse.
+ * @param {Array<FieldDef>=} fieldDefs FieldDefs from a project config, used to
+ *   mask labels that are used to implement custom enum fields.
+ */
+function TKR_convertLabels(labelsResponse, fieldDefs = []) {
+  const labelDefs = (labelsResponse.labelDefs || []);
+  const exclusiveLabelPrefixes = (labelsResponse.exclusiveLabelPrefixes || []);
+  const jsonData = {};
+
+  const maskedLabels = new Set();
+  fieldDefs.forEach((fd) => {
+    if (fd.enumChoices) {
+      fd.enumChoices.forEach(({label}) => {
+        maskedLabels.add(`${fd.fieldRef.fieldName}-${label}`);
+      });
+    }
+  });
+
+  jsonData.labels = labelDefs.filter(({label}) => !maskedLabels.has(label)).map(
+      (label) => ({name: label.label, doc: label.docstring}));
+
+  jsonData.excl_prefixes = exclusiveLabelPrefixes.map(
+      (prefix) => prefix.toLowerCase());
+
+  return jsonData;
+}
+
+
+/**
+ * Convert the object resulting of a monorail.Projects GetVisibleMembers
+ * call to the format expected by TKR_populateAutocomplete.
+ * @param {object?} visibleMembersResponse A pRPC GetVisibleMembersResponse.
+ * @return {{memberEmails: {name: string}, nonGroupEmails: {name: string}}}
+ */
+function TKR_convertVisibleMembers(visibleMembersResponse) {
+  if (!visibleMembersResponse) {
+    visibleMembersResponse = {};
+  }
+  const groupRefs = (visibleMembersResponse.groupRefs || []);
+  const userRefs = (visibleMembersResponse.userRefs || []);
+  const jsonData = {};
+
+  const groupEmails = new Set(groupRefs.map(
+      (groupRef) => groupRef.displayName));
+
+  jsonData.memberEmails = userRefs.map(
+      (userRef) => ({name: userRef.displayName}));
+  jsonData.nonGroupEmails = jsonData.memberEmails.filter(
+      (memberEmail) => !groupEmails.has(memberEmail));
+
+  return jsonData;
+}
+
+
+/**
+ * Convert the object resulting of a monorail.Projects ListFields to
+ * the format expected by TKR_populateAutocomplete.
+ * @param {object} fieldsResponse A pRPC ListFieldsResponse object.
+ */
+function TKR_convertFields(fieldsResponse) {
+  const fieldDefs = (fieldsResponse.fieldDefs || []);
+  const jsonData = {};
+
+  jsonData.fields = fieldDefs.map((field) =>
+    ({
+      field_id: field.fieldRef.fieldId,
+      field_name: field.fieldRef.fieldName,
+      field_type: field.fieldRef.type,
+      docstring: field.docstring,
+      choices: (field.enumChoices || []).map(
+          (choice) => ({name: choice.label, doc: choice.docstring})),
+      qualifiedMembers: (field.userChoices || []).map(
+          (userRef) => ({name: userRef.displayName})),
+    }),
+  );
+
+  return jsonData;
+}
+
+
+/**
+ * Convert the object resulting of a monorail.Features ListHotlistsByUser
+ * call to the format expected by TKR_populateAutocomplete.
+ * @param {Array<HotlistV0>} hotlists A lists of hotlists
+ * @return {Array<{ref_str: string, summary: string}>}
+ */
+function TKR_convertHotlists(hotlists) {
+  if (hotlists === undefined) {
+    return [];
+  }
+
+  const seen = new Set();
+  const ambiguousNames = new Set();
+
+  hotlists.forEach((hotlist) => {
+    if (seen.has(hotlist.name)) {
+      ambiguousNames.add(hotlist.name);
+    }
+    seen.add(hotlist.name);
+  });
+
+  return hotlists.map((hotlist) => {
+    let ref_str = hotlist.name;
+    if (ambiguousNames.has(hotlist.name)) {
+      ref_str = hotlist.owner_ref.display_name + ':' + ref_str;
+    }
+    return {ref_str: ref_str, summary: hotlist.summary};
+  });
+}
+
+
+/**
+ * Initializes hotlists in autocomplete store.
+ * @param {Array<HotlistV0>} hotlists
+ */
+function TKR_populateHotlistAutocomplete(hotlists) {
+  TKR_setUpHotlistsStore(TKR_convertHotlists(hotlists));
+}
+
+
+/**
+ * Add project config data that's already been fetched to the legacy
+ * autocomplete.
+ * @param {Config} projectConfig Returned projectConfig data.
+ * @param {GetVisibleMembersResponse} visibleMembers
+ * @param {Array<string>} customPermissions
+ */
+function TKR_populateAutocomplete(projectConfig, visibleMembers,
+    customPermissions = []) {
+  const {statusDefs, componentDefs, labelDefs, fieldDefs,
+    exclusiveLabelPrefixes, projectName} = projectConfig;
+
+  const {memberEmails, nonGroupEmails} =
+    TKR_convertVisibleMembers(visibleMembers);
+  TKR_setUpMemberStore(memberEmails, nonGroupEmails);
+  TKR_prepOwnerField(memberEmails);
+
+  const {open, closed, strict} = TKR_convertStatuses({statusDefs});
+  TKR_setUpStatusStore(open, closed);
+  TKR_restrict_to_known = strict;
+
+  const {components} = TKR_convertComponents({componentDefs});
+  TKR_setUpComponentStore(components);
+
+  const {excl_prefixes, labels} = TKR_convertLabels(
+      {labelDefs, exclusiveLabelPrefixes}, fieldDefs);
+  TKR_exclPrefixes = excl_prefixes;
+  TKR_setUpLabelStore(labels);
+
+  const {fields} = TKR_convertFields({fieldDefs});
+  TKR_setUpUserAutocompleteStores(fields);
+
+  /* QuickEdit is not yet in Monorail. crbug.com/monorail/1926
+  TKR_setUpQuickEditStore(
+      jsonData.labels, jsonData.memberEmails, jsonData.open, jsonData.closed,
+      jsonData.nonGroupEmails);
+  */
+
+  // We need to wait until both exclusive prefixes (in configPromise) and
+  // labels (in labelsPromise) have been read.
+  TKR_prepLabelAC(TKR_labelFieldIDPrefix);
+
+  TKR_setUpSearchStore(
+      labels, memberEmails, open, closed,
+      components, fields, nonGroupEmails);
+
+  TKR_setUpCustomPermissionsStore(customPermissions);
+}