Merge "Fix template problems with gr-confirm-delete-comment-dialog"
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 7ddf2ba..f476566 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -369,11 +369,11 @@
     if (commonServer != null) {
       try {
         commonServer.close();
-      } catch (Throwable t) {
+      } catch (Exception e) {
         throw new AssertionError(
             "Error stopping common server in "
                 + (firstTest != null ? firstTest.getTestClass().getName() : "unknown test class"),
-            t);
+            e);
       } finally {
         commonServer = null;
       }
diff --git a/java/com/google/gerrit/entities/Change.java b/java/com/google/gerrit/entities/Change.java
index ca13db9..d1826bc 100644
--- a/java/com/google/gerrit/entities/Change.java
+++ b/java/com/google/gerrit/entities/Change.java
@@ -443,9 +443,6 @@
   /** Globally assigned unique identifier of the change */
   protected Key changeKey;
 
-  /** optimistic locking */
-  protected int rowVersion;
-
   /** When this change was first introduced into the database. */
   protected Timestamp createdOn;
 
@@ -526,7 +523,6 @@
     assignee = other.assignee;
     changeId = other.changeId;
     changeKey = other.changeKey;
-    rowVersion = other.rowVersion;
     createdOn = other.createdOn;
     lastUpdatedOn = other.lastUpdatedOn;
     owner = other.owner;
@@ -587,10 +583,6 @@
     lastUpdatedOn = now;
   }
 
-  public int getRowVersion() {
-    return rowVersion;
-  }
-
   public Account.Id getOwner() {
     return owner;
   }
diff --git a/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
index 25e68f9..689b4aa 100644
--- a/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
@@ -43,7 +43,6 @@
     Entities.Change.Builder builder =
         Entities.Change.newBuilder()
             .setChangeId(changeIdConverter.toProto(change.getId()))
-            .setRowVersion(change.getRowVersion())
             .setChangeKey(changeKeyConverter.toProto(change.getKey()))
             .setCreatedOn(change.getCreatedOn().getTime())
             .setLastUpdatedOn(change.getLastUpdatedOn().getTime())
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index 5cc8e3c..b727e96 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -36,6 +36,8 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.ResultSet;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.change.MergeabilityComputationBehavior;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.account.AccountIndex;
@@ -50,6 +52,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import org.eclipse.jgit.lib.Config;
 
 /**
  * Fake secondary index implementation for usage in tests. All values are kept in-memory.
@@ -179,14 +182,17 @@
   public static class FakeChangeIndex
       extends AbstractFakeIndex<Change.Id, ChangeData, Map<String, Object>> implements ChangeIndex {
     private final ChangeData.Factory changeDataFactory;
+    private final boolean skipMergable;
 
     @Inject
     FakeChangeIndex(
         SitePaths sitePaths,
         ChangeData.Factory changeDataFactory,
-        @Assisted Schema<ChangeData> schema) {
+        @Assisted Schema<ChangeData> schema,
+        @GerritServerConfig Config cfg) {
       super(schema, sitePaths, "changes");
       this.changeDataFactory = changeDataFactory;
+      this.skipMergable = !MergeabilityComputationBehavior.fromConfig(cfg).includeInIndex();
     }
 
     @Override
@@ -208,6 +214,9 @@
     protected Map<String, Object> docFor(ChangeData value) {
       ImmutableMap.Builder<String, Object> doc = ImmutableMap.builder();
       for (FieldDef<ChangeData, ?> field : getSchema().getFields().values()) {
+        if (ChangeField.MERGEABLE.getName().equals(field.getName()) && skipMergable) {
+          continue;
+        }
         Object docifiedValue = field.get(value);
         if (docifiedValue != null) {
           doc.put(field.getName(), field.get(value));
diff --git a/java/com/google/gerrit/lifecycle/LifecycleManager.java b/java/com/google/gerrit/lifecycle/LifecycleManager.java
index 4f09a09..42123d7 100644
--- a/java/com/google/gerrit/lifecycle/LifecycleManager.java
+++ b/java/com/google/gerrit/lifecycle/LifecycleManager.java
@@ -107,7 +107,7 @@
       LifecycleListener obj = listeners.get(i).get();
       try {
         obj.stop();
-      } catch (Throwable err) {
+      } catch (RuntimeException err) {
         logger.atWarning().withCause(err).log("Failed to stop %s", obj.getClass());
       }
       startedIndex = i - 1;
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 2b4cfef..a3605f7 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -310,7 +310,7 @@
         RuntimeShutdown.waitFor();
       }
       return 0;
-    } catch (Throwable err) {
+    } catch (RuntimeException err) {
       logger.atSevere().withCause(err).log("Unable to start daemon");
       return 1;
     }
diff --git a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 89b4228..6f3514f 100644
--- a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -547,7 +547,7 @@
           filterHolder.setInitParameters(initParams);
         }
         app.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));
-      } catch (Throwable e) {
+      } catch (Exception e) {
         throw new IllegalArgumentException(
             "Unable to instantiate front-end HTTP Filter " + filterClassName, e);
       }
diff --git a/java/com/google/gerrit/server/RequestCleanup.java b/java/com/google/gerrit/server/RequestCleanup.java
index f405c57..e07d148 100644
--- a/java/com/google/gerrit/server/RequestCleanup.java
+++ b/java/com/google/gerrit/server/RequestCleanup.java
@@ -44,7 +44,7 @@
       for (Iterator<Runnable> i = cleanup.iterator(); i.hasNext(); ) {
         try {
           i.next().run();
-        } catch (Throwable err) {
+        } catch (Exception err) {
           logger.atSevere().withCause(err).log("Failed to execute per-request cleanup");
         }
         i.remove();
diff --git a/java/com/google/gerrit/server/change/AbandonUtil.java b/java/com/google/gerrit/server/change/AbandonUtil.java
index 1bc1fad..d030ec1 100644
--- a/java/com/google/gerrit/server/change/AbandonUtil.java
+++ b/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -93,7 +93,7 @@
         try {
           batchAbandon.batchAbandon(updateFactory, project, internalUser, changes, message);
           count += changes.size();
-        } catch (Throwable e) {
+        } catch (Exception e) {
           StringBuilder msg = new StringBuilder("Failed to auto-abandon inactive change(s):");
           for (ChangeData change : changes) {
             msg.append(" ").append(change.getId().get());
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index 27b71d6..0d0df0d 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -167,7 +167,6 @@
   public void prepareETag(Hasher h, CurrentUser user) {
     h.putInt(JSON_FORMAT_VERSION)
         .putLong(getChange().getLastUpdatedOn().getTime())
-        .putInt(getChange().getRowVersion())
         .putInt(user.isIdentifiedUser() ? user.getAccountId().get() : 0);
 
     if (user.isIdentifiedUser()) {
diff --git a/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
index cae213f..2823548 100644
--- a/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
+++ b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
@@ -145,7 +145,7 @@
       }
       groupUuids = newGroupUuids;
       logger.atInfo().log("Run group indexer, %s groups reindexed", reindexCounter);
-    } catch (Throwable t) {
+    } catch (Exception t) {
       logger.atSevere().withCause(t).log("Failed to reindex groups");
     }
   }
diff --git a/java/com/google/gerrit/server/logging/PerformanceLogContext.java b/java/com/google/gerrit/server/logging/PerformanceLogContext.java
index b6dafdc..65e033b15 100644
--- a/java/com/google/gerrit/server/logging/PerformanceLogContext.java
+++ b/java/com/google/gerrit/server/logging/PerformanceLogContext.java
@@ -92,7 +92,7 @@
             p -> {
               try (TraceContext traceContext = newPluginTrace(p)) {
                 performanceLogRecords.forEach(r -> r.writeTo(p.get()));
-              } catch (Throwable e) {
+              } catch (RuntimeException e) {
                 logger.atWarning().withCause(e).log(
                     "Failure in %s of plugin %s", p.get().getClass(), p.getPluginName());
               }
diff --git a/java/com/google/gerrit/server/plugincontext/PluginContext.java b/java/com/google/gerrit/server/plugincontext/PluginContext.java
index 90d56c8..1cfee65 100644
--- a/java/com/google/gerrit/server/plugincontext/PluginContext.java
+++ b/java/com/google/gerrit/server/plugincontext/PluginContext.java
@@ -204,7 +204,7 @@
     try (TraceContext traceContext = newTrace(extension);
         Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
       extensionImplConsumer.run(extensionImpl);
-    } catch (Throwable e) {
+    } catch (Exception e) {
       pluginMetrics.incrementErrorCount(extension);
       logger.atWarning().withCause(e).log(
           "Failure in %s of plugin %s", extensionImpl.getClass(), extension.getPluginName());
@@ -233,7 +233,7 @@
     try (TraceContext traceContext = newTrace(extension);
         Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
       extensionConsumer.run(extension);
-    } catch (Throwable e) {
+    } catch (Exception e) {
       pluginMetrics.incrementErrorCount(extension);
       logger.atWarning().withCause(e).log(
           "Failure in %s of plugin %s", extensionImpl.getClass(), extension.getPluginName());
@@ -267,7 +267,7 @@
     try (TraceContext traceContext = newTrace(extension);
         Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
       extensionImplConsumer.run(extensionImpl);
-    } catch (Throwable e) {
+    } catch (Exception e) {
       Throwables.throwIfInstanceOf(e, exceptionClass);
       Throwables.throwIfUnchecked(e);
       pluginMetrics.incrementErrorCount(extension);
@@ -304,7 +304,7 @@
     try (TraceContext traceContext = newTrace(extension);
         Timer3.Context<String, String, String> ctx = pluginMetrics.startLatency(extension)) {
       extensionConsumer.run(extension);
-    } catch (Throwable e) {
+    } catch (Exception e) {
       Throwables.throwIfInstanceOf(e, exceptionClass);
       Throwables.throwIfUnchecked(e);
       pluginMetrics.incrementErrorCount(extension);
diff --git a/java/com/google/gerrit/server/plugins/PluginLoader.java b/java/com/google/gerrit/server/plugins/PluginLoader.java
index 0a06081..8d17d85 100644
--- a/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -253,7 +253,7 @@
           FileSnapshot snapshot = FileSnapshot.save(off.toFile());
           Plugin offPlugin = loadPlugin(name, off, snapshot);
           disabled.put(name, offPlugin);
-        } catch (Throwable e) {
+        } catch (Exception e) {
           // This shouldn't happen, as the plugin was loaded earlier.
           logger.atWarning().withCause(e.getCause()).log(
               "Cannot load disabled plugin %s", active.getName());
@@ -510,7 +510,7 @@
       if (!newPlugin.isDisabled()) {
         try {
           newPlugin.start(env);
-        } catch (Throwable e) {
+        } catch (Exception e) {
           newPlugin.stop(env);
           throw e;
         }
@@ -528,7 +528,7 @@
       }
       broken.remove(name);
       return newPlugin;
-    } catch (Throwable err) {
+    } catch (Exception err) {
       broken.put(name, snapshot);
       throw new PluginInstallException(err);
     }
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 852387f..8b4e4c7 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -38,7 +38,6 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.Change;
@@ -1287,12 +1286,6 @@
     return refStates;
   }
 
-  @UsedAt(UsedAt.Project.GOOGLE)
-  public void setRefStates(Iterable<byte[]> refStates) {
-    // TODO(hanwen): remove Google use, and drop this method.
-    setRefStates(RefState.parseStates(refStates));
-  }
-
   public void setRefStates(ImmutableSetMultimap<Project.NameKey, RefState> refStates) {
     this.refStates = refStates;
     if (draftsByUser == null) {
diff --git a/java/com/google/gerrit/server/rules/PrologEnvironment.java b/java/com/google/gerrit/server/rules/PrologEnvironment.java
index 1a563ad..7d626da 100644
--- a/java/com/google/gerrit/server/rules/PrologEnvironment.java
+++ b/java/com/google/gerrit/server/rules/PrologEnvironment.java
@@ -143,7 +143,7 @@
     for (Iterator<Runnable> i = cleanup.iterator(); i.hasNext(); ) {
       try {
         i.next().run();
-      } catch (Throwable err) {
+      } catch (Exception err) {
         logger.atSevere().withCause(err).log("Failed to execute cleanup for PrologEnvironment");
       }
       i.remove();
diff --git a/java/com/google/gerrit/server/update/RetryableChangeAction.java b/java/com/google/gerrit/server/update/RetryableChangeAction.java
index 152db2c..84ec2bb 100644
--- a/java/com/google/gerrit/server/update/RetryableChangeAction.java
+++ b/java/com/google/gerrit/server/update/RetryableChangeAction.java
@@ -82,11 +82,11 @@
   public T call() throws UpdateException, RestApiException {
     try {
       return super.call();
-    } catch (Throwable t) {
-      Throwables.throwIfUnchecked(t);
-      Throwables.throwIfInstanceOf(t, UpdateException.class);
-      Throwables.throwIfInstanceOf(t, RestApiException.class);
-      throw new UpdateException(t);
+    } catch (Exception e) {
+      Throwables.throwIfUnchecked(e);
+      Throwables.throwIfInstanceOf(e, UpdateException.class);
+      Throwables.throwIfInstanceOf(e, RestApiException.class);
+      throw new UpdateException(e);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/update/RetryableIndexQueryAction.java b/java/com/google/gerrit/server/update/RetryableIndexQueryAction.java
index cf733a6..d66edcf 100644
--- a/java/com/google/gerrit/server/update/RetryableIndexQueryAction.java
+++ b/java/com/google/gerrit/server/update/RetryableIndexQueryAction.java
@@ -87,9 +87,9 @@
   public T call() {
     try {
       return super.call();
-    } catch (Throwable t) {
-      Throwables.throwIfUnchecked(t);
-      throw new StorageException(t);
+    } catch (Exception e) {
+      Throwables.throwIfUnchecked(e);
+      throw new StorageException(e);
     }
   }
 }
diff --git a/java/com/google/gerrit/sshd/BaseCommand.java b/java/com/google/gerrit/sshd/BaseCommand.java
index 48a5512..42aabfb 100644
--- a/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/java/com/google/gerrit/sshd/BaseCommand.java
@@ -370,7 +370,7 @@
         err.flush();
       } catch (IOException e2) {
         // Ignored
-      } catch (Throwable e2) {
+      } catch (RuntimeException e2) {
         logger.atWarning().withCause(e2).log("Cannot send failure message to client");
       }
       return f.exitCode;
@@ -381,7 +381,7 @@
       err.flush();
     } catch (IOException e2) {
       // Ignored
-    } catch (Throwable e2) {
+    } catch (RuntimeException e2) {
       logger.atWarning().withCause(e2).log("Cannot send internal server error message to client");
     }
     return 128;
@@ -500,15 +500,15 @@
 
           out.flush();
           err.flush();
-        } catch (Throwable e) {
+        } catch (Exception e) {
           try {
             out.flush();
-          } catch (Throwable e2) {
+          } catch (Exception e2) {
             // Ignored
           }
           try {
             err.flush();
-          } catch (Throwable e2) {
+          } catch (Exception e2) {
             // Ignored
           }
           rc = handleError(e);
diff --git a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 773c25b..d5f0ee8 100644
--- a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -138,7 +138,7 @@
         // to do with the key object, and instead we must abort this load.
         //
         throw e;
-      } catch (Throwable e) {
+      } catch (Exception e) {
         markInvalid(k);
       }
     }
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 0eda433..c1f4a7b 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -214,16 +214,16 @@
       } catch (GitAPIException e) {
         throw new Failure(7, "fatal: git api exception, " + e);
       }
-    } catch (Throwable t) {
+    } catch (Exception e) {
       // Report the error in ERROR sideband channel. Catch Throwable too so we can also catch
       // NoClassDefFound.
       try (SideBandOutputStream sidebandError =
           new SideBandOutputStream(
               SideBandOutputStream.CH_ERROR, SideBandOutputStream.MAX_BUF, out)) {
-        sidebandError.write(t.getMessage().getBytes(UTF_8));
+        sidebandError.write(e.getMessage().getBytes(UTF_8));
         sidebandError.flush();
       }
-      throw t;
+      throw e;
     } finally {
       // In any case, cleanly close the packetOut channel
       packetOut.end();
diff --git a/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
index ae8e06d..8c5e449 100644
--- a/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
@@ -60,7 +60,6 @@
         Entities.Change.newBuilder()
             .setChangeId(Entities.Change_Id.newBuilder().setId(14))
             .setChangeKey(Entities.Change_Key.newBuilder().setId("change 1"))
-            .setRowVersion(0)
             .setCreatedOn(987654L)
             .setLastUpdatedOn(1234567L)
             .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
@@ -109,7 +108,6 @@
                     .setBranch("refs/heads/branch-74"))
             // Default values which can't be unset.
             .setCurrentPatchSetId(0)
-            .setRowVersion(0)
             .setStatus(Change.STATUS_NEW)
             .setIsPrivate(false)
             .setWorkInProgress(false)
@@ -147,7 +145,6 @@
                     .setBranch("refs/heads/branch-74"))
             .setCurrentPatchSetId(0)
             // Default values which can't be unset.
-            .setRowVersion(0)
             .setStatus(Change.STATUS_NEW)
             .setIsPrivate(false)
             .setWorkInProgress(false)
@@ -185,7 +182,6 @@
             .setCurrentPatchSetId(23)
             .setSubject("subject ABC")
             // Default values which can't be unset.
-            .setRowVersion(0)
             .setStatus(Change.STATUS_NEW)
             .setIsPrivate(false)
             .setWorkInProgress(false)
@@ -251,7 +247,6 @@
     assertThat(change.getSubject()).isNull();
     assertThat(change.currentPatchSetId()).isNull();
     // Default values for unset protobuf fields which can't be unset in the entity object.
-    assertThat(change.getRowVersion()).isEqualTo(0);
     assertThat(change.isNew()).isTrue();
     assertThat(change.isPrivate()).isFalse();
     assertThat(change.isWorkInProgress()).isFalse();
@@ -284,7 +279,6 @@
             ImmutableMap.<String, Type>builder()
                 .put("changeId", Change.Id.class)
                 .put("changeKey", Change.Key.class)
-                .put("rowVersion", int.class)
                 .put("createdOn", Timestamp.class)
                 .put("lastUpdatedOn", Timestamp.class)
                 .put("owner", Account.Id.class)
@@ -309,7 +303,6 @@
   private static void assertEqualChange(Change change, Change expectedChange) {
     assertThat(change.getChangeId()).isEqualTo(expectedChange.getChangeId());
     assertThat(change.getKey()).isEqualTo(expectedChange.getKey());
-    assertThat(change.getRowVersion()).isEqualTo(expectedChange.getRowVersion());
     assertThat(change.getCreatedOn()).isEqualTo(expectedChange.getCreatedOn());
     assertThat(change.getLastUpdatedOn()).isEqualTo(expectedChange.getLastUpdatedOn());
     assertThat(change.getOwner()).isEqualTo(expectedChange.getOwner());
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index 287a7fe..10599c6 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -64,6 +64,7 @@
             cfg.setInt("change", null, "maxFiles", 2);
             cfg.setInt("change", null, "maxPatchSets", MAX_PATCH_SETS);
             cfg.setInt("change", null, "maxUpdates", MAX_UPDATES);
+            cfg.setString("index", null, "type", "fake");
             return cfg;
           });
 
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index d685bab..6a64126 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -95,9 +95,6 @@
     "elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.ts",
     "elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts",
     "elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.ts",
-    "elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.ts",
-    "elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts",
-    "elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.ts",
     "elements/change/gr-file-list-header/gr-file-list-header_html.ts",
     "elements/change/gr-file-list/gr-file-list_html.ts",
     "elements/change/gr-label-score-row/gr-label-score-row_html.ts",
@@ -106,7 +103,6 @@
     "elements/change/gr-reply-dialog/gr-reply-dialog_html.ts",
     "elements/change/gr-reviewer-list/gr-reviewer-list_html.ts",
     "elements/change/gr-thread-list/gr-thread-list_html.ts",
-    "elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts",
     "elements/diff/gr-diff-builder/gr-diff-builder-element_html.ts",
     "elements/diff/gr-diff-host/gr-diff-host_html.ts",
     "elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts",
@@ -122,7 +118,6 @@
     "elements/shared/gr-comment-thread/gr-comment-thread_html.ts",
     "elements/shared/gr-comment/gr-comment_html.ts",
     "elements/shared/gr-dialog/gr-dialog_html.ts",
-    "elements/shared/gr-diff-preferences/gr-diff-preferences_html.ts",
     "elements/shared/gr-download-commands/gr-download-commands_html.ts",
     "elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts",
     "elements/shared/gr-dropdown/gr-dropdown_html.ts",
@@ -131,7 +126,6 @@
     "elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.ts",
     "elements/shared/gr-list-view/gr-list-view_html.ts",
     "elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.ts",
-    "elements/shared/gr-textarea/gr-textarea_html.ts",
 ]
 
 # Transform templates into a .ts files.
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index 4e6c963..77b7717 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -27,8 +27,9 @@
   AutocompleteSuggestion,
 } from '../../shared/gr-autocomplete/gr-autocomplete';
 import {appContext} from '../../../services/app-context';
+import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 
-interface RebaseChange {
+export interface RebaseChange {
   name: string;
   value: NumericChangeId;
 }
@@ -39,10 +40,15 @@
 
 export interface GrConfirmRebaseDialog {
   $: {
+    confirmDialog: GrDialog;
     parentInput: GrAutocomplete;
+    parentUpToDateMsg: HTMLDivElement;
+    rebaseOnParent: HTMLDivElement;
     rebaseOnParentInput: HTMLInputElement;
     rebaseOnOtherInput: HTMLInputElement;
+    rebaseOnTip: HTMLDivElement;
     rebaseOnTipInput: HTMLInputElement;
+    tipUpToDateMsg: HTMLDivElement;
   };
 }
 
@@ -77,10 +83,10 @@
   rebaseOnCurrent?: boolean;
 
   @property({type: String})
-  _text?: string;
+  _text = '';
 
   @property({type: Object})
-  _query?: AutocompleteQuery;
+  _query: AutocompleteQuery = () => Promise.resolve([]);
 
   @property({type: Array})
   _recentChanges?: RebaseChange[];
@@ -146,15 +152,15 @@
       );
   }
 
-  _displayParentOption(rebaseOnCurrent: boolean, hasParent: boolean) {
+  _displayParentOption(rebaseOnCurrent?: boolean, hasParent?: boolean) {
     return hasParent && rebaseOnCurrent;
   }
 
-  _displayParentUpToDateMsg(rebaseOnCurrent: boolean, hasParent: boolean) {
+  _displayParentUpToDateMsg(rebaseOnCurrent?: boolean, hasParent?: boolean) {
     return hasParent && !rebaseOnCurrent;
   }
 
-  _displayTipOption(rebaseOnCurrent: boolean, hasParent: boolean) {
+  _displayTipOption(rebaseOnCurrent?: boolean, hasParent?: boolean) {
     return !(!rebaseOnCurrent && !hasParent);
   }
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.ts
index bed9240..1052201 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.ts
@@ -79,7 +79,6 @@
           name="rebaseOptions"
           type="radio"
           disabled$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]"
-          on-click="_handleRebaseOnTip"
         />
         <label id="rebaseOnTipLabel" for="rebaseOnTipInput">
           Rebase on top of the [[branch]] branch<span hidden$="[[!hasParent]]">
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
similarity index 67%
rename from polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.js
rename to polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
index 0faf604..74c1b3c 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
@@ -15,14 +15,18 @@
  * limitations under the License.
  */
 
-import '../../../test/common-test-setup-karma.js';
-import './gr-confirm-rebase-dialog.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import '../../../test/common-test-setup-karma';
+import './gr-confirm-rebase-dialog';
+import {GrConfirmRebaseDialog, RebaseChange} from './gr-confirm-rebase-dialog';
+import {stubRestApi} from '../../../test/test-utils';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {NumericChangeId} from '../../../types/common';
+import {createChangeViewChange} from '../../../test/test-data-generators';
 
 const basicFixture = fixtureFromElement('gr-confirm-rebase-dialog');
 
 suite('gr-confirm-rebase-dialog tests', () => {
-  let element;
+  let element: GrConfirmRebaseDialog;
 
   setup(() => {
     element = basicFixture.instantiate();
@@ -75,16 +79,20 @@
   test('input cleared on cancel or submit', () => {
     element._text = '123';
     element.$.confirmDialog.dispatchEvent(
-        new CustomEvent('confirm', {
-          composed: true, bubbles: true,
-        }));
+      new CustomEvent('confirm', {
+        composed: true,
+        bubbles: true,
+      })
+    );
     assert.equal(element._text, '');
 
     element._text = '123';
     element.$.confirmDialog.dispatchEvent(
-        new CustomEvent('cancel', {
-          composed: true, bubbles: true,
-        }));
+      new CustomEvent('cancel', {
+        composed: true,
+        bubbles: true,
+      })
+    );
     assert.equal(element._text, '');
   });
 
@@ -102,55 +110,59 @@
   });
 
   suite('parent suggestions', () => {
-    let recentChanges;
-    let getChangesStub;
+    let recentChanges: RebaseChange[];
+    let getChangesStub: sinon.SinonStub;
     setup(() => {
       recentChanges = [
         {
           name: '123: my first awesome change',
-          value: 123,
+          value: 123 as NumericChangeId,
         },
         {
           name: '124: my second awesome change',
-          value: 124,
+          value: 124 as NumericChangeId,
         },
         {
           name: '245: my third awesome change',
-          value: 245,
+          value: 245 as NumericChangeId,
         },
       ];
 
-      getChangesStub = stubRestApi('getChanges').returns(Promise.resolve(
-          [
-            {
-              _number: 123,
-              subject: 'my first awesome change',
-            },
-            {
-              _number: 124,
-              subject: 'my second awesome change',
-            },
-            {
-              _number: 245,
-              subject: 'my third awesome change',
-            },
-          ]
-      ));
+      getChangesStub = stubRestApi('getChanges').returns(
+        Promise.resolve([
+          {
+            ...createChangeViewChange(),
+            _number: 123 as NumericChangeId,
+            subject: 'my first awesome change',
+          },
+          {
+            ...createChangeViewChange(),
+            _number: 124 as NumericChangeId,
+            subject: 'my second awesome change',
+          },
+          {
+            ...createChangeViewChange(),
+            _number: 245 as NumericChangeId,
+            subject: 'my third awesome change',
+          },
+        ])
+      );
     });
 
     test('_getRecentChanges', () => {
-      sinon.spy(element, '_getRecentChanges');
-      return element._getRecentChanges()
-          .then(() => {
-            assert.deepEqual(element._recentChanges, recentChanges);
-            assert.equal(getChangesStub.callCount, 1);
-            // When called a second time, should not re-request recent changes.
-            element._getRecentChanges();
-          })
-          .then(() => {
-            assert.equal(element._getRecentChanges.callCount, 2);
-            assert.equal(getChangesStub.callCount, 1);
-          });
+      const recentChangesSpy = sinon.spy(element, '_getRecentChanges');
+      return element
+        ._getRecentChanges()
+        .then(() => {
+          assert.deepEqual(element._recentChanges, recentChanges);
+          assert.equal(getChangesStub.callCount, 1);
+          // When called a second time, should not re-request recent changes.
+          element._getRecentChanges();
+        })
+        .then(() => {
+          assert.equal(recentChangesSpy.callCount, 2);
+          assert.equal(getChangesStub.callCount, 1);
+        });
     });
 
     test('_filterChanges', () => {
@@ -159,25 +171,25 @@
       assert.equal(element._filterChanges('awesome', recentChanges).length, 3);
       assert.equal(element._filterChanges('third', recentChanges).length, 1);
 
-      element.changeNumber = 123;
+      element.changeNumber = 123 as NumericChangeId;
       assert.equal(element._filterChanges('123', recentChanges).length, 0);
       assert.equal(element._filterChanges('124', recentChanges).length, 1);
       assert.equal(element._filterChanges('awesome', recentChanges).length, 2);
     });
 
     test('input text change triggers function', () => {
-      sinon.spy(element, '_getRecentChanges');
+      const recentChangesSpy = sinon.spy(element, '_getRecentChanges');
       element.$.parentInput.noDebounce = true;
       MockInteractions.pressAndReleaseKeyOn(
-          element.$.parentInput.$.input,
-          13,
-          null,
-          'enter');
+        element.$.parentInput.$.input,
+        13,
+        null,
+        'enter'
+      );
       element._text = '1';
-      assert.isTrue(element._getRecentChanges.calledOnce);
+      assert.isTrue(recentChangesSpy.calledOnce);
       element._text = '12';
-      assert.isTrue(element._getRecentChanges.calledTwice);
+      assert.isTrue(recentChangesSpy.calledTwice);
     });
   });
 });
-
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
index 450551b..b971039 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
@@ -59,7 +59,7 @@
   /* The revert message updated by the user
       The default value is set by the dialog */
   @property({type: String})
-  _message?: string;
+  _message = '';
 
   @property({type: Number})
   _revertType = RevertType.REVERT_SINGLE_CHANGE;
@@ -198,7 +198,7 @@
     this._message = this._revertMessages[RevertType.REVERT_SUBMISSION];
   }
 
-  _handleConfirmTap(e: MouseEvent) {
+  _handleConfirmTap(e: Event) {
     e.preventDefault();
     e.stopPropagation();
     if (this._message === this._originalRevertMessages[this._revertType]) {
@@ -218,7 +218,7 @@
     );
   }
 
-  _handleCancelTap(e: MouseEvent) {
+  _handleCancelTap(e: Event) {
     e.preventDefault();
     e.stopPropagation();
     this.dispatchEvent(
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts
index 3ec4f2c..b2acff2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts
@@ -85,8 +85,8 @@
           <label for="revertSubmission" class="label revertSubmission">
             Revert entire submission ([[_changesCount]] Changes)
           </label>
-        </div></template
-      >
+        </div>
+      </template>
       <gr-endpoint-decorator name="confirm-revert-change">
         <label for="messageInput"> Revert Commit Message </label>
         <iron-autogrow-textarea
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
index 33f7304..bd8eaac 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
@@ -85,19 +85,20 @@
     return commentThreads.filter(thread => isUnresolved(thread));
   }
 
-  _computeUnresolvedCommentsWarning(change: ChangeInfo) {
+  _computeUnresolvedCommentsWarning(change?: ChangeInfo) {
+    if (!change) return '';
     const unresolvedCount = change.unresolved_comment_count;
     if (!unresolvedCount) throw new Error('unresolved comments undefined or 0');
     return `Heads Up! ${pluralize(unresolvedCount, 'unresolved comment')}.`;
   }
 
-  _handleConfirmTap(e: MouseEvent) {
+  _handleConfirmTap(e: Event) {
     e.preventDefault();
     e.stopPropagation();
     this.dispatchEvent(new CustomEvent('confirm', {bubbles: false}));
   }
 
-  _handleCancelTap(e: MouseEvent) {
+  _handleCancelTap(e: Event) {
     e.preventDefault();
     e.stopPropagation();
     this.dispatchEvent(new CustomEvent('cancel', {bubbles: false}));
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 5206fdb..f4f37c1 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -82,13 +82,9 @@
 import {Timing} from '../../../constants/reporting';
 import {RevisionInfo} from '../../shared/revision-info/revision-info';
 import {preferences$} from '../../../services/user/user-model';
-import {
-  changeComments$,
-  drafts$,
-} from '../../../services/comments/comments-model';
+import {changeComments$} from '../../../services/comments/comments-model';
 import {Subject} from 'rxjs';
 import {takeUntil} from 'rxjs/operators';
-import {UIDraft} from '../../../utils/comment-util';
 
 export const DEFAULT_NUM_FILES_SHOWN = 200;
 
@@ -316,9 +312,6 @@
   @property({type: Array})
   _dynamicPrependedContentEndpoints?: string[];
 
-  @property({type: Object})
-  diffDrafts?: {[path: string]: UIDraft[]} = {};
-
   private readonly reporting = appContext.reportingService;
 
   private readonly restApiService = appContext.restApiService;
@@ -373,9 +366,6 @@
   /** @override */
   connectedCallback() {
     super.connectedCallback();
-    drafts$.pipe(takeUntil(this.disconnected$)).subscribe(drafts => {
-      this.diffDrafts = drafts;
-    });
     changeComments$
       .pipe(takeUntil(this.disconnected$))
       .subscribe(changeComments => {
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index 5e6b076..d5c0171 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -33,6 +33,7 @@
   AccountDetailInfo,
   AccountInfo,
   ChangeInfo,
+  NumericChangeId,
   UrlEncodedCommentId,
 } from '../../../types/common';
 import {
@@ -82,7 +83,7 @@
   threads: CommentThread[] = [];
 
   @property({type: String})
-  changeNum?: string;
+  changeNum?: NumericChangeId;
 
   @property({type: Boolean})
   loggedIn?: boolean;
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 5cd7bfc..e60c2bb 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -97,7 +97,7 @@
       '_computeDisableApplyFixButton(_isApplyFixLoading, change, ' +
       '_patchNum)',
   })
-  _disableApplyFixButton?: boolean;
+  _disableApplyFixButton = false;
 
   layers = appContext.flagsService.isEnabled(
     KnownExperimentId.TOKEN_HIGHLIGHTING
@@ -187,12 +187,13 @@
     return (_fixSuggestions || []).length === 1;
   }
 
-  overridePartialPrefs(prefs: DiffPreferencesInfo): DiffPreferencesInfo {
+  overridePartialPrefs(prefs?: DiffPreferencesInfo) {
+    if (!prefs) return undefined;
     // generate a smaller gr-diff than fullscreen for dialog
     return {...prefs, line_length: 50};
   }
 
-  onCancel(e: CustomEvent) {
+  onCancel(e: Event) {
     if (e) {
       e.stopPropagation();
     }
@@ -203,7 +204,7 @@
     return _selectedFixIdx + 1;
   }
 
-  _onPrevFixClick(e: CustomEvent) {
+  _onPrevFixClick(e: Event) {
     if (e) e.stopPropagation();
     if (this._selectedFixIdx >= 1 && this._fixSuggestions) {
       this._selectedFixIdx -= 1;
@@ -213,7 +214,7 @@
     }
   }
 
-  _onNextFixClick(e: CustomEvent) {
+  _onNextFixClick(e: Event) {
     if (e) e.stopPropagation();
     if (
       this._fixSuggestions &&
@@ -257,7 +258,7 @@
   }
 
   _computeDisableApplyFixButton(
-    isApplyFixLoading?: boolean,
+    isApplyFixLoading: boolean,
     change?: ParsedChangeInfo,
     patchNum?: PatchSetNum
   ) {
@@ -271,7 +272,7 @@
     return isApplyFixLoading;
   }
 
-  _handleApplyFix(e: CustomEvent) {
+  _handleApplyFix(e: Event) {
     if (e) {
       e.stopPropagation();
     }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index fdde355..888e514 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -40,7 +40,12 @@
 } from './gr-diff-utils';
 import {getHiddenScroll} from '../../../scripts/hiddenscroll';
 import {customElement, observe, property} from '@polymer/decorators';
-import {BlameInfo, CommentRange, ImageInfo} from '../../../types/common';
+import {
+  BlameInfo,
+  CommentRange,
+  ImageInfo,
+  NumericChangeId,
+} from '../../../types/common';
 import {
   DiffInfo,
   DiffPreferencesInfo,
@@ -155,7 +160,7 @@
    */
 
   @property({type: String})
-  changeNum?: string;
+  changeNum?: NumericChangeId;
 
   @property({type: Boolean})
   noAutoRender = false;
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index da75914..16d54dd 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -28,7 +28,7 @@
   GenerateUrlEditViewParameters,
 } from '../../core/gr-navigation/gr-navigation';
 import {computeTruncatedPath} from '../../../utils/path-list-util';
-import {customElement, property} from '@polymer/decorators';
+import {customElement, observe, property} from '@polymer/decorators';
 import {
   ChangeInfo,
   PatchSetNum,
@@ -211,8 +211,8 @@
   }
 
   _editChange(value?: ChangeInfo | null) {
-    if (!changeIsMerged(value) && !changeIsAbandoned(value)) return;
     if (!value) return;
+    if (!changeIsMerged(value) && !changeIsAbandoned(value)) return;
     fireAlert(
       this,
       'Change edits cannot be created if change is merged or abandoned. Redirected to non edit mode.'
@@ -220,6 +220,15 @@
     GerritNav.navigateToChange(value);
   }
 
+  @observe('_change', '_type')
+  _editType(change?: ChangeInfo | null, type?: string) {
+    if (!change || !type || !type.startsWith('image/')) return;
+
+    // Prevent editing binary files
+    fireAlert(this, 'You cannot edit binary files within the inline editor.');
+    GerritNav.navigateToChange(change);
+  }
+
   _handlePathChanged(e: CustomEvent<string>) {
     // TODO(TS) could be cleaned up, it was added for type requirements
     if (this._changeNum === undefined || !this._path) {
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
index 4909bef..7cfd1b3 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
@@ -35,6 +35,7 @@
     showMatchBrackets: HTMLInputElement;
     editShowLineWrapping: HTMLInputElement;
     editShowTabs: HTMLInputElement;
+    editShowTrailingWhitespaceInput: HTMLInputElement;
   };
 }
 @customElement('gr-edit-preferences')
@@ -89,6 +90,14 @@
     this._handleEditPrefsChanged();
   }
 
+  _handleEditShowTrailingWhitespaceTap() {
+    this.set(
+      'editPrefs.show_whitespace_errors',
+      this.$.editShowTrailingWhitespaceInput.checked
+    );
+    this._handleEditPrefsChanged();
+  }
+
   _handleMatchBracketsChanged() {
     this.set('editPrefs.match_brackets', this.$.showMatchBrackets.checked);
     this._handleEditPrefsChanged();
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.ts b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.ts
index a51deaf..abd925f 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.ts
@@ -85,6 +85,19 @@
       </span>
     </section>
     <section>
+      <label for="showTrailingWhitespaceInput" class="title"
+        >Show trailing whitespace</label
+      >
+      <span class="value">
+        <input
+          id="editShowTrailingWhitespaceInput"
+          type="checkbox"
+          checked$="[[editPrefs.show_whitespace_errors]]"
+          on-change="_handleEditShowTrailingWhitespaceTap"
+        />
+      </span>
+    </section>
+    <section>
       <label for="showMatchBrackets" class="title">Match brackets</label>
       <span class="value">
         <input
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
index 73d1bf0..4d2aee7 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
@@ -39,7 +39,7 @@
   }
 }
 
-interface Item {
+export interface Item {
   dataValue?: string;
   name?: string;
   text?: string;
@@ -47,6 +47,11 @@
   value?: string;
 }
 
+export interface ItemSelectedEvent {
+  trigger: string;
+  selected: HTMLElement | null;
+}
+
 @customElement('gr-autocomplete-dropdown')
 export class GrAutocompleteDropdown extends IronFitMixin(
   KeyboardShortcutMixin(PolymerElement),
@@ -155,7 +160,7 @@
     e.preventDefault();
     e.stopPropagation();
     this.dispatchEvent(
-      new CustomEvent('item-selected', {
+      new CustomEvent<ItemSelectedEvent>('item-selected', {
         detail: {
           trigger: 'tab',
           selected: this.cursor.target,
@@ -170,7 +175,7 @@
     e.preventDefault();
     e.stopPropagation();
     this.dispatchEvent(
-      new CustomEvent('item-selected', {
+      new CustomEvent<ItemSelectedEvent>('item-selected', {
         detail: {
           trigger: 'enter',
           selected: this.cursor.target,
@@ -189,7 +194,7 @@
   _handleClickItem(e: Event) {
     e.preventDefault();
     e.stopPropagation();
-    let selected = e.target! as Element;
+    let selected = e.target! as HTMLElement;
     while (!selected.classList.contains('autocompleteOption')) {
       if (!selected || selected === this) {
         return;
@@ -197,7 +202,7 @@
       selected = selected.parentElement!;
     }
     this.dispatchEvent(
-      new CustomEvent('item-selected', {
+      new CustomEvent<ItemSelectedEvent>('item-selected', {
         detail: {
           trigger: 'click',
           selected,
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 2ad3be6..afff6f3 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -225,18 +225,6 @@
     this.addEventListener('comment-update', e =>
       this._handleCommentUpdate(e as CustomEvent)
     );
-    // Wait for comment to be rendered before scrolling to it
-    if (this.shouldScrollIntoView) {
-      const resizeObserver = new ResizeObserver(
-        (_entries: ResizeObserverEntry[], observer: ResizeObserver) => {
-          if (this.offsetHeight > 0) {
-            this.scrollIntoView();
-            observer.unobserve(this);
-          }
-        }
-      );
-      resizeObserver.observe(this);
-    }
   }
 
   /** @override */
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
index 70a7bf3..e560773 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
@@ -21,18 +21,23 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-diff-preferences_html';
 import {customElement, property} from '@polymer/decorators';
-import {DiffPreferencesInfo} from '../../../types/diff';
+import {DiffPreferencesInfo, IgnoreWhitespaceType} from '../../../types/diff';
 import {GrSelect} from '../gr-select/gr-select';
 import {appContext} from '../../../services/app-context';
 
 export interface GrDiffPreferences {
   $: {
+    contextLineSelect: HTMLInputElement;
+    columnsInput: HTMLInputElement;
+    tabSizeInput: HTMLInputElement;
+    fontSizeInput: HTMLInputElement;
     lineWrappingInput: HTMLInputElement;
     showTabsInput: HTMLInputElement;
     showTrailingWhitespaceInput: HTMLInputElement;
     automaticReviewInput: HTMLInputElement;
     syntaxHighlightInput: HTMLInputElement;
     contextSelect: GrSelect;
+    ignoreWhiteSpace: HTMLInputElement;
   };
   save(): Promise<void>;
 }
@@ -61,11 +66,31 @@
     this.hasUnsavedChanges = true;
   }
 
+  _handleDiffContextChanged() {
+    this.set('diffPrefs.context', Number(this.$.contextLineSelect.value));
+    this._handleDiffPrefsChanged();
+  }
+
   _handleLineWrappingTap() {
     this.set('diffPrefs.line_wrapping', this.$.lineWrappingInput.checked);
     this._handleDiffPrefsChanged();
   }
 
+  _handleDiffLineLengthChanged() {
+    this.set('diffPrefs.line_length', Number(this.$.columnsInput.value));
+    this._handleDiffPrefsChanged();
+  }
+
+  _handleDiffTabSizeChanged() {
+    this.set('diffPrefs.tab_size', Number(this.$.tabSizeInput.value));
+    this._handleDiffPrefsChanged();
+  }
+
+  _handleDiffFontSizeChanged() {
+    this.set('diffPrefs.font_size', Number(this.$.fontSizeInput.value));
+    this._handleDiffPrefsChanged();
+  }
+
   _handleShowTabsTap() {
     this.set('diffPrefs.show_tabs', this.$.showTabsInput.checked);
     this._handleDiffPrefsChanged();
@@ -92,6 +117,14 @@
     this._handleDiffPrefsChanged();
   }
 
+  _handleDiffIgnoreWhitespaceChanged() {
+    this.set(
+      'diffPrefs.ignore_whitespace',
+      this.$.ignoreWhiteSpace.value as IgnoreWhitespaceType
+    );
+    this._handleDiffPrefsChanged();
+  }
+
   save() {
     if (!this.diffPrefs)
       return Promise.reject(new Error('Missing diff preferences'));
@@ -99,6 +132,27 @@
       this.hasUnsavedChanges = false;
     });
   }
+
+  /**
+   * bind-value has type string so we have to convert
+   * anything inputed to string.
+   *
+   * This is so typescript checker doesn't fail.
+   */
+  _convertToString(key?: number | IgnoreWhitespaceType) {
+    return key !== undefined ? String(key) : '';
+  }
+
+  /**
+   * input 'checked' does not allow undefined,
+   * so we make sure the value is boolean
+   * by returning false if undefined.
+   *
+   * This is so typescript checker doesn't fail.
+   */
+  _convertToBoolean(key?: boolean) {
+    return key !== undefined ? key : false;
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.ts
index ed3d695..51867c8 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.ts
@@ -27,12 +27,12 @@
     <section>
       <label for="contextLineSelect" class="title">Context</label>
       <span class="value">
-        <gr-select id="contextSelect" bind-value="{{diffPrefs.context}}">
-          <select
-            id="contextLineSelect"
-            on-keypress="_handleDiffPrefsChanged"
-            on-change="_handleDiffPrefsChanged"
-          >
+        <gr-select
+          id="contextSelect"
+          bind-value="[[_convertToString(diffPrefs.context)]]"
+          on-change="_handleDiffContextChanged"
+        >
+          <select id="contextLineSelect">
             <option value="3">3 lines</option>
             <option value="10">10 lines</option>
             <option value="25">25 lines</option>
@@ -50,7 +50,7 @@
         <input
           id="lineWrappingInput"
           type="checkbox"
-          checked="[[diffPrefs.line_wrapping]]"
+          checked="[[_convertToBoolean(diffPrefs.line_wrapping)]]"
           on-change="_handleLineWrappingTap"
         />
       </span>
@@ -59,23 +59,11 @@
       <label for="columnsInput" class="title">Diff width</label>
       <span class="value">
         <iron-input
-          type="number"
-          prevent-invalid-input=""
           allowed-pattern="[0-9]"
-          bind-value="{{diffPrefs.line_length}}"
-          on-keypress="_handleDiffPrefsChanged"
-          on-change="_handleDiffPrefsChanged"
+          bind-value="[[_convertToString(diffPrefs.line_length)]]"
+          on-change="_handleDiffLineLengthChanged"
         >
-          <input
-            is="iron-input"
-            type="number"
-            id="columnsInput"
-            prevent-invalid-input=""
-            allowed-pattern="[0-9]"
-            bind-value="{{diffPrefs.line_length}}"
-            on-keypress="_handleDiffPrefsChanged"
-            on-change="_handleDiffPrefsChanged"
-          />
+          <input id="columnsInput" type="number" />
         </iron-input>
       </span>
     </section>
@@ -83,23 +71,11 @@
       <label for="tabSizeInput" class="title">Tab width</label>
       <span class="value">
         <iron-input
-          type="number"
-          prevent-invalid-input=""
           allowed-pattern="[0-9]"
-          bind-value="{{diffPrefs.tab_size}}"
-          on-keypress="_handleDiffPrefsChanged"
-          on-change="_handleDiffPrefsChanged"
+          bind-value="[[_convertToString(diffPrefs.tab_size)]]"
+          on-change="_handleDiffTabSizeChanged"
         >
-          <input
-            is="iron-input"
-            type="number"
-            id="tabSizeInput"
-            prevent-invalid-input=""
-            allowed-pattern="[0-9]"
-            bind-value="{{diffPrefs.tab_size}}"
-            on-keypress="_handleDiffPrefsChanged"
-            on-change="_handleDiffPrefsChanged"
-          />
+          <input id="tabSizeInput" type="number" />
         </iron-input>
       </span>
     </section>
@@ -107,23 +83,11 @@
       <label for="fontSizeInput" class="title">Font size</label>
       <span class="value">
         <iron-input
-          type="number"
-          prevent-invalid-input=""
           allowed-pattern="[0-9]"
-          bind-value="{{diffPrefs.font_size}}"
-          on-keypress="_handleDiffPrefsChanged"
-          on-change="_handleDiffPrefsChanged"
+          bind-value="[[_convertToString(diffPrefs.font_size)]]"
+          on-change="_handleDiffFontSizeChanged"
         >
-          <input
-            is="iron-input"
-            type="number"
-            id="fontSizeInput"
-            prevent-invalid-input=""
-            allowed-pattern="[0-9]"
-            bind-value="{{diffPrefs.font_size}}"
-            on-keypress="_handleDiffPrefsChanged"
-            on-change="_handleDiffPrefsChanged"
-          />
+          <input id="fontSizeInput" type="number" />
         </iron-input>
       </span>
     </section>
@@ -133,7 +97,7 @@
         <input
           id="showTabsInput"
           type="checkbox"
-          checked="[[diffPrefs.show_tabs]]"
+          checked="[[_convertToBoolean(diffPrefs.show_tabs)]]"
           on-change="_handleShowTabsTap"
         />
       </span>
@@ -146,7 +110,7 @@
         <input
           id="showTrailingWhitespaceInput"
           type="checkbox"
-          checked="[[diffPrefs.show_whitespace_errors]]"
+          checked="[[_convertToBoolean(diffPrefs.show_whitespace_errors)]]"
           on-change="_handleShowTrailingWhitespaceTap"
         />
       </span>
@@ -159,7 +123,7 @@
         <input
           id="syntaxHighlightInput"
           type="checkbox"
-          checked="[[diffPrefs.syntax_highlighting]]"
+          checked="[[_convertToBoolean(diffPrefs.syntax_highlighting)]]"
           on-change="_handleSyntaxHighlightTap"
         />
       </span>
@@ -172,7 +136,7 @@
         <input
           id="automaticReviewInput"
           type="checkbox"
-          checked="[[!diffPrefs.manual_review]]"
+          checked="[[!_convertToBoolean(diffPrefs.manual_review)]]"
           on-change="_handleAutomaticReviewTap"
         />
       </span>
@@ -181,12 +145,11 @@
       <div class="pref">
         <label for="ignoreWhiteSpace" class="title">Ignore Whitespace</label>
         <span class="value">
-          <gr-select bind-value="{{diffPrefs.ignore_whitespace}}">
-            <select
-              id="ignoreWhiteSpace"
-              on-keypress="_handleDiffPrefsChanged"
-              on-change="_handleDiffPrefsChanged"
-            >
+          <gr-select
+            bind-value="[[_convertToString(diffPrefs.ignore_whitespace)]]"
+            on-change="_handleDiffIgnoreWhitespaceChanged"
+          >
+            <select id="ignoreWhiteSpace">
               <option value="IGNORE_NONE">None</option>
               <option value="IGNORE_TRAILING">Trailing</option>
               <option value="IGNORE_LEADING_AND_TRAILING">
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.js
deleted file mode 100644
index 716ef2f..0000000
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.js
+++ /dev/null
@@ -1,105 +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.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-diff-preferences.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-diff-preferences');
-
-suite('gr-diff-preferences tests', () => {
-  let element;
-
-  let diffPreferences;
-
-  function valueOf(title, fieldsetid) {
-    const sections = element.$[fieldsetid].querySelectorAll('section');
-    let titleEl;
-    for (let i = 0; i < sections.length; i++) {
-      titleEl = sections[i].querySelector('.title');
-      if (titleEl.textContent.trim() === title) {
-        return sections[i].querySelector('.value');
-      }
-    }
-  }
-
-  setup(() => {
-    diffPreferences = {
-      context: 10,
-      line_wrapping: false,
-      line_length: 100,
-      tab_size: 8,
-      font_size: 12,
-      show_tabs: true,
-      show_whitespace_errors: true,
-      syntax_highlighting: true,
-      manual_review: false,
-      ignore_whitespace: 'IGNORE_NONE',
-    };
-
-    stubRestApi('getDiffPreferences').returns(Promise.resolve(diffPreferences));
-
-    element = basicFixture.instantiate();
-
-    return element.loadData();
-  });
-
-  test('renders', () => {
-    // Rendered with the expected preferences selected.
-    assert.equal(valueOf('Context', 'diffPreferences')
-        .firstElementChild.bindValue, diffPreferences.context);
-    assert.equal(valueOf('Fit to screen', 'diffPreferences')
-        .firstElementChild.checked, diffPreferences.line_wrapping);
-    assert.equal(valueOf('Diff width', 'diffPreferences')
-        .firstElementChild.bindValue, diffPreferences.line_length);
-    assert.equal(valueOf('Tab width', 'diffPreferences')
-        .firstElementChild.bindValue, diffPreferences.tab_size);
-    assert.equal(valueOf('Font size', 'diffPreferences')
-        .firstElementChild.bindValue, diffPreferences.font_size);
-    assert.equal(valueOf('Show tabs', 'diffPreferences')
-        .firstElementChild.checked, diffPreferences.show_tabs);
-    assert.equal(valueOf('Show trailing whitespace', 'diffPreferences')
-        .firstElementChild.checked, diffPreferences.show_whitespace_errors);
-    assert.equal(valueOf('Syntax highlighting', 'diffPreferences')
-        .firstElementChild.checked, diffPreferences.syntax_highlighting);
-    assert.equal(
-        valueOf('Automatically mark viewed files reviewed', 'diffPreferences')
-            .firstElementChild.checked, !diffPreferences.manual_review);
-    assert.equal(valueOf('Ignore Whitespace', 'diffPreferences')
-        .firstElementChild.bindValue, diffPreferences.ignore_whitespace);
-
-    assert.isFalse(element.hasUnsavedChanges);
-  });
-
-  test('save changes', () => {
-    stubRestApi('saveDiffPreferences')
-        .returns(Promise.resolve());
-    const showTrailingWhitespaceCheckbox =
-        valueOf('Show trailing whitespace', 'diffPreferences')
-            .firstElementChild;
-    showTrailingWhitespaceCheckbox.checked = false;
-    element._handleShowTrailingWhitespaceTap();
-
-    assert.isTrue(element.hasUnsavedChanges);
-
-    // Save the change.
-    return element.save().then(() => {
-      assert.isFalse(element.hasUnsavedChanges);
-    });
-  });
-});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
new file mode 100644
index 0000000..6c1404e
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
@@ -0,0 +1,134 @@
+/**
+ * @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.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-diff-preferences';
+import {GrDiffPreferences} from './gr-diff-preferences';
+import {stubRestApi} from '../../../test/test-utils';
+import {DiffPreferencesInfo} from '../../../types/diff';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {IronInputElement} from '@polymer/iron-input';
+import {GrSelect} from '../gr-select/gr-select';
+
+const basicFixture = fixtureFromElement('gr-diff-preferences');
+
+suite('gr-diff-preferences tests', () => {
+  let element: GrDiffPreferences;
+
+  let diffPreferences: DiffPreferencesInfo;
+
+  function valueOf(title: string, id: string) {
+    const sections = element.root?.querySelectorAll(`#${id} section`) ?? [];
+    let titleEl;
+    for (let i = 0; i < sections.length; i++) {
+      titleEl = sections[i].querySelector('.title');
+      if (titleEl?.textContent?.trim() === title) {
+        const el = sections[i].querySelector('.value');
+        if (el) return el;
+      }
+    }
+    assert.fail(`element with title ${title} not found`);
+  }
+
+  setup(async () => {
+    diffPreferences = createDefaultDiffPrefs();
+
+    stubRestApi('getDiffPreferences').returns(Promise.resolve(diffPreferences));
+
+    element = basicFixture.instantiate();
+
+    await element.loadData();
+    await flush();
+  });
+
+  test('renders', () => {
+    // Rendered with the expected preferences selected.
+    const contextInput = valueOf('Context', 'diffPreferences')
+      .firstElementChild as IronInputElement;
+    assert.equal(contextInput.bindValue, `${diffPreferences.context}`);
+
+    const lineWrappingInput = valueOf('Fit to screen', 'diffPreferences')
+      .firstElementChild as HTMLInputElement;
+    assert.equal(lineWrappingInput.checked, diffPreferences.line_wrapping);
+
+    const lineLengthInput = valueOf('Diff width', 'diffPreferences')
+      .firstElementChild as IronInputElement;
+    assert.equal(lineLengthInput.bindValue, `${diffPreferences.line_length}`);
+
+    const tabSizeInput = valueOf('Tab width', 'diffPreferences')
+      .firstElementChild as IronInputElement;
+    assert.equal(tabSizeInput.bindValue, `${diffPreferences.tab_size}`);
+
+    const fontSizeInput = valueOf('Font size', 'diffPreferences')
+      .firstElementChild as IronInputElement;
+    assert.equal(fontSizeInput.bindValue, `${diffPreferences.font_size}`);
+
+    const showTabsInput = valueOf('Show tabs', 'diffPreferences')
+      .firstElementChild as HTMLInputElement;
+    assert.equal(showTabsInput.checked, diffPreferences.show_tabs);
+
+    const showWhitespaceErrorsInput = valueOf(
+      'Show trailing whitespace',
+      'diffPreferences'
+    ).firstElementChild as HTMLInputElement;
+    assert.equal(
+      showWhitespaceErrorsInput.checked,
+      diffPreferences.show_whitespace_errors
+    );
+
+    const syntaxHighlightingInput = valueOf(
+      'Syntax highlighting',
+      'diffPreferences'
+    ).firstElementChild as HTMLInputElement;
+    assert.equal(
+      syntaxHighlightingInput.checked,
+      diffPreferences.syntax_highlighting
+    );
+
+    const manualReviewInput = valueOf(
+      'Automatically mark viewed files reviewed',
+      'diffPreferences'
+    ).firstElementChild as HTMLInputElement;
+    assert.equal(manualReviewInput.checked, !diffPreferences.manual_review);
+
+    const ignoreWhitespaceInput = valueOf(
+      'Ignore Whitespace',
+      'diffPreferences'
+    ).firstElementChild as GrSelect;
+    assert.equal(
+      ignoreWhitespaceInput.bindValue,
+      diffPreferences.ignore_whitespace
+    );
+
+    assert.isFalse(element.hasUnsavedChanges);
+  });
+
+  test('save changes', async () => {
+    const showTrailingWhitespaceCheckbox = valueOf(
+      'Show trailing whitespace',
+      'diffPreferences'
+    ).firstElementChild as HTMLInputElement;
+    showTrailingWhitespaceCheckbox.checked = false;
+    element._handleShowTrailingWhitespaceTap();
+
+    assert.isTrue(element.hasUnsavedChanges);
+
+    // Save the change.
+    await element.save();
+    assert.isFalse(element.hasUnsavedChanges);
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index a747ac4..17c46c6 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -28,7 +28,12 @@
 import {customElement, property} from '@polymer/decorators';
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
+import {
+  GrAutocompleteDropdown,
+  Item,
+  ItemSelectedEvent,
+} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
+import {CustomKeyboardEvent} from '../../../types/events';
 
 const MAX_ITEMS_DROPDOWN = 10;
 
@@ -56,11 +61,8 @@
   {value: '😜', match: 'winking tongue ;)'},
 ];
 
-interface EmojiSuggestion {
-  value: string;
+interface EmojiSuggestion extends Item {
   match: string;
-  dataValue?: string;
-  text?: string;
 }
 
 interface ValueChangeEvent {
@@ -76,6 +78,13 @@
   };
 }
 
+declare global {
+  interface HTMLElementEventMap {
+    'item-selected': CustomEvent<ItemSelectedEvent>;
+    'bind-value-changed': CustomEvent<ValueChangeEvent>;
+  }
+}
+
 @customElement('gr-textarea')
 export class GrTextarea extends KeyboardShortcutMixin(PolymerElement) {
   static get template() {
@@ -85,8 +94,8 @@
   /**
    * @event bind-value-changed
    */
-  @property({type: Boolean})
-  autocomplete?: boolean;
+  @property({type: String})
+  autocomplete?: string;
 
   @property({type: Boolean})
   disabled?: boolean;
@@ -101,7 +110,7 @@
   placeholder?: string;
 
   @property({type: String, notify: true, observer: '_handleTextChanged'})
-  text?: string;
+  text = '';
 
   @property({type: Boolean})
   hideBorder = false;
@@ -125,10 +134,10 @@
   _hideEmojiAutocomplete = true;
 
   @property({type: Number})
-  _index?: number;
+  _index: number | null = null;
 
   @property({type: Array})
-  _suggestions?: EmojiSuggestion[];
+  _suggestions: EmojiSuggestion[] = [];
 
   @property({type: Number})
   readonly _verticalOffset = 20;
@@ -227,11 +236,14 @@
     this._setEmoji(this.$.emojiSuggestions.getCurrentText());
   }
 
-  _handleEnterByKey(e: CustomEvent<{keyboardEvent: KeyboardEvent}>) {
+  _handleEnterByKey(e: CustomKeyboardEvent) {
     // Enter should have newline behavior if the picker is closed or if the user
     // has only typed ':'. Also make sure that shortcuts aren't clobbered.
     if (this._hideEmojiAutocomplete || this.disableEnterKeyForSelectingEmoji) {
-      if (!e.detail.keyboardEvent.metaKey && !e.detail.keyboardEvent.ctrlKey) {
+      if (
+        !e.detail.keyboardEvent?.metaKey &&
+        !e.detail.keyboardEvent?.ctrlKey
+      ) {
         this.indent(e);
       }
       return;
@@ -242,8 +254,10 @@
     this._setEmoji(this.$.emojiSuggestions.getCurrentText());
   }
 
-  _handleEmojiSelect(e: CustomEvent) {
-    this._setEmoji(e.detail.selected.dataset['value']);
+  _handleEmojiSelect(e: CustomEvent<ItemSelectedEvent>) {
+    if (e.detail.selected?.dataset['value']) {
+      this._setEmoji(e.detail.selected?.dataset['value']);
+    }
   }
 
   _setEmoji(text: string) {
@@ -369,7 +383,7 @@
     const suggestions = [];
     for (const suggestion of matchedSuggestions) {
       suggestion.dataValue = suggestion.value;
-      suggestion.text = suggestion.value + ' ' + suggestion.match;
+      suggestion.text = `${suggestion.value} ${suggestion.match}`;
       suggestions.push(suggestion);
     }
     this.set('_suggestions', suggestions);
@@ -404,7 +418,7 @@
     );
   }
 
-  private indent(e: CustomEvent<{keyboardEvent: KeyboardEvent}>): void {
+  private indent(e: CustomKeyboardEvent): void {
     if (!document.queryCommandSupported('insertText')) {
       return;
     }
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js
deleted file mode 100644
index 7c2f209..0000000
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js
+++ /dev/null
@@ -1,373 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-textarea.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-const basicFixture = fixtureFromElement('gr-textarea');
-
-const monospaceFixture = fixtureFromTemplate(html`
-<gr-textarea monospace="true"></gr-textarea>
-`);
-
-const hideBorderFixture = fixtureFromTemplate(html`
-<gr-textarea hide-border="true"></gr-textarea>
-`);
-
-suite('gr-textarea tests', () => {
-  let element;
-
-  setup(() => {
-    element = basicFixture.instantiate();
-    sinon.stub(element.reporting, 'reportInteraction');
-  });
-
-  test('monospace is set properly', () => {
-    assert.isFalse(element.classList.contains('monospace'));
-  });
-
-  test('hideBorder is set properly', () => {
-    assert.isFalse(element.$.textarea.classList.contains('noBorder'));
-  });
-
-  test('emoji selector is not open with the textarea lacks focus', () => {
-    element.$.textarea.selectionStart = 1;
-    element.$.textarea.selectionEnd = 1;
-    element.text = ':';
-    assert.isFalse(!element.$.emojiSuggestions.isHidden);
-  });
-
-  test('emoji selector is not open when a general text is entered', () => {
-    MockInteractions.focus(element.$.textarea);
-    element.$.textarea.selectionStart = 9;
-    element.$.textarea.selectionEnd = 9;
-    element.text = 'some text';
-    assert.isFalse(!element.$.emojiSuggestions.isHidden);
-  });
-
-  test('emoji selector opens when a colon is typed & the textarea has focus',
-      () => {
-        MockInteractions.focus(element.$.textarea);
-        // Needed for Safari tests. selectionStart is not updated when text is
-        // updated.
-        element.$.textarea.selectionStart = 1;
-        element.$.textarea.selectionEnd = 1;
-        element.text = ':';
-        flush();
-        assert.isFalse(element.$.emojiSuggestions.isHidden);
-        assert.equal(element._colonIndex, 0);
-        assert.isFalse(element._hideEmojiAutocomplete);
-        assert.equal(element._currentSearchString, '');
-      });
-
-  test('emoji selector opens when a colon is typed after space',
-      () => {
-        MockInteractions.focus(element.$.textarea);
-        // Needed for Safari tests. selectionStart is not updated when text is
-        // updated.
-        element.$.textarea.selectionStart = 2;
-        element.$.textarea.selectionEnd = 2;
-        element.text = ' :';
-        flush();
-        assert.isFalse(element.$.emojiSuggestions.isHidden);
-        assert.equal(element._colonIndex, 1);
-        assert.isFalse(element._hideEmojiAutocomplete);
-        assert.equal(element._currentSearchString, '');
-      });
-
-  test('emoji selector doesn\`t open when a colon is typed after character',
-      () => {
-        MockInteractions.focus(element.$.textarea);
-        // Needed for Safari tests. selectionStart is not updated when text is
-        // updated.
-        element.$.textarea.selectionStart = 5;
-        element.$.textarea.selectionEnd = 5;
-        element.text = 'test:';
-        flush();
-        assert.isTrue(element.$.emojiSuggestions.isHidden);
-        assert.isTrue(element._hideEmojiAutocomplete);
-      });
-
-  test('emoji selector opens when a colon is typed and some substring',
-      () => {
-        MockInteractions.focus(element.$.textarea);
-        // Needed for Safari tests. selectionStart is not updated when text is
-        // updated.
-        element.$.textarea.selectionStart = 1;
-        element.$.textarea.selectionEnd = 1;
-        element.text = ':';
-        element.$.textarea.selectionStart = 2;
-        element.$.textarea.selectionEnd = 2;
-        element.text = ':t';
-        flush();
-        assert.isFalse(element.$.emojiSuggestions.isHidden);
-        assert.equal(element._colonIndex, 0);
-        assert.isFalse(element._hideEmojiAutocomplete);
-        assert.equal(element._currentSearchString, 't');
-      });
-
-  test('emoji selector opens when a colon is typed in middle of text',
-      () => {
-        MockInteractions.focus(element.$.textarea);
-        // Needed for Safari tests. selectionStart is not updated when text is
-        // updated.
-        element.$.textarea.selectionStart = 1;
-        element.$.textarea.selectionEnd = 1;
-        // Since selectionStart is on Chrome set always on end of text, we
-        // stub it to 1
-        const text = ': hello';
-        sinon.stub(element.$, 'textarea').value( {
-          selectionStart: 1,
-          value: text,
-          textarea: {
-            focus: () => {},
-          },
-        });
-        element.text = text;
-        flush();
-        assert.isFalse(element.$.emojiSuggestions.isHidden);
-        assert.equal(element._colonIndex, 0);
-        assert.isFalse(element._hideEmojiAutocomplete);
-        assert.equal(element._currentSearchString, '');
-      });
-  test('emoji selector closes when text changes before the colon', () => {
-    const resetStub = sinon.stub(element, '_resetEmojiDropdown');
-    MockInteractions.focus(element.$.textarea);
-    flush();
-    element.$.textarea.selectionStart = 10;
-    element.$.textarea.selectionEnd = 10;
-    element.text = 'test test ';
-    element.$.textarea.selectionStart = 12;
-    element.$.textarea.selectionEnd = 12;
-    element.text = 'test test :';
-    element.$.textarea.selectionStart = 15;
-    element.$.textarea.selectionEnd = 15;
-    element.text = 'test test :smi';
-
-    assert.equal(element._currentSearchString, 'smi');
-    assert.isFalse(resetStub.called);
-    element.text = 'test test test :smi';
-    assert.isTrue(resetStub.called);
-  });
-
-  test('_resetEmojiDropdown', () => {
-    const closeSpy = sinon.spy(element, 'closeDropdown');
-    element._resetEmojiDropdown();
-    assert.equal(element._currentSearchString, '');
-    assert.isTrue(element._hideEmojiAutocomplete);
-    assert.equal(element._colonIndex, null);
-
-    element.$.emojiSuggestions.open();
-    flush();
-    element._resetEmojiDropdown();
-    assert.isTrue(closeSpy.called);
-  });
-
-  test('_determineSuggestions', () => {
-    const emojiText = 'tear';
-    const formatSpy = sinon.spy(element, '_formatSuggestions');
-    element._determineSuggestions(emojiText);
-    assert.isTrue(formatSpy.called);
-    assert.isTrue(formatSpy.lastCall.calledWithExactly(
-        [{dataValue: '😂', value: '😂', match: 'tears :\')',
-          text: '😂 tears :\')'},
-        {dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'},
-        ]));
-  });
-
-  test('_formatSuggestions', () => {
-    const matchedSuggestions = [{value: '😢', match: 'tear'},
-      {value: '😂', match: 'tears'}];
-    element._formatSuggestions(matchedSuggestions);
-    assert.deepEqual(
-        [{value: '😢', dataValue: '😢', match: 'tear', text: '😢 tear'},
-          {value: '😂', dataValue: '😂', match: 'tears', text: '😂 tears'}],
-        element._suggestions);
-  });
-
-  test('_handleEmojiSelect', () => {
-    element.$.textarea.selectionStart = 16;
-    element.$.textarea.selectionEnd = 16;
-    element.text = 'test test :tears';
-    element._colonIndex = 10;
-    const selectedItem = {dataset: {value: '😂'}};
-    const event = {detail: {selected: selectedItem}};
-    element._handleEmojiSelect(event);
-    assert.equal(element.text, 'test test 😂');
-  });
-
-  test('_updateCaratPosition', () => {
-    element.$.textarea.selectionStart = 4;
-    element.$.textarea.selectionEnd = 4;
-    element.text = 'test';
-    element._updateCaratPosition();
-    assert.deepEqual(element.$.hiddenText.innerHTML, element.text +
-        element.$.caratSpan.outerHTML);
-  });
-
-  test('newline receives matching indentation', async () => {
-    const indentCommand = sinon.stub(document, 'execCommand');
-    element.$.textarea.value = '    a';
-    element._handleEnterByKey(
-        new CustomEvent('keydown', {detail: {keyboardEvent: {keyCode: 13}}})
-    );
-    await flush();
-    assert.deepEqual(indentCommand.args[0], ['insertText', false, '\n    ']);
-  });
-
-  test('ctrl+enter and meta+enter do not indent', async () => {
-    const indentCommand = sinon.stub(document, 'execCommand');
-    element.$.textarea.value = '    a';
-    element._handleEnterByKey(
-        new CustomEvent('keydown', {
-          detail: {keyboardEvent: {keyCode: 13, ctrlKey: true}},
-        })
-    );
-    await flush();
-    assert.isTrue(indentCommand.notCalled);
-
-    element._handleEnterByKey(
-        new CustomEvent('keydown', {
-          detail: {keyboardEvent: {keyCode: 13, metaKey: true}},
-        })
-    );
-    await flush();
-    assert.isTrue(indentCommand.notCalled);
-  });
-
-  test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
-    const resetSpy = sinon.spy(element, '_resetEmojiDropdown');
-    element.$.emojiSuggestions.dispatchEvent(
-        new CustomEvent('dropdown-closed', {
-          composed: true, bubbles: true,
-        }));
-    assert.isTrue(resetSpy.called);
-  });
-
-  test('_onValueChanged fires bind-value-changed', () => {
-    const listenerStub = sinon.stub();
-    const eventObject = {currentTarget: {focused: false}};
-    element.addEventListener('bind-value-changed', listenerStub);
-    element._onValueChanged(eventObject);
-    assert.isTrue(listenerStub.called);
-  });
-
-  suite('keyboard shortcuts', () => {
-    function setupDropdown(callback) {
-      MockInteractions.focus(element.$.textarea);
-      element.$.textarea.selectionStart = 1;
-      element.$.textarea.selectionEnd = 1;
-      element.text = ':';
-      element.$.textarea.selectionStart = 1;
-      element.$.textarea.selectionEnd = 2;
-      element.text = ':1';
-      flush();
-    }
-
-    test('escape key', () => {
-      const resetSpy = sinon.spy(element, '_resetEmojiDropdown');
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
-      assert.isFalse(resetSpy.called);
-      setupDropdown();
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
-      assert.isTrue(resetSpy.called);
-      assert.isFalse(!element.$.emojiSuggestions.isHidden);
-    });
-
-    test('up key', () => {
-      const upSpy = sinon.spy(element.$.emojiSuggestions, 'cursorUp');
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
-      assert.isFalse(upSpy.called);
-      setupDropdown();
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
-      assert.isTrue(upSpy.called);
-    });
-
-    test('down key', () => {
-      const downSpy = sinon.spy(element.$.emojiSuggestions, 'cursorDown');
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
-      assert.isFalse(downSpy.called);
-      setupDropdown();
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
-      assert.isTrue(downSpy.called);
-    });
-
-    test('enter key', () => {
-      const enterSpy = sinon.spy(element.$.emojiSuggestions,
-          'getCursorTarget');
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
-      assert.isFalse(enterSpy.called);
-      setupDropdown();
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
-      assert.isTrue(enterSpy.called);
-      flush();
-      assert.equal(element.text, '💯');
-    });
-
-    test('enter key - ignored on just colon without more information', () => {
-      const enterSpy = sinon.spy(element.$.emojiSuggestions,
-          'getCursorTarget');
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
-      assert.isFalse(enterSpy.called);
-      MockInteractions.focus(element.$.textarea);
-      element.$.textarea.selectionStart = 1;
-      element.$.textarea.selectionEnd = 1;
-      element.text = ':';
-      flush();
-      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
-      assert.isFalse(enterSpy.called);
-    });
-  });
-
-  suite('gr-textarea monospace', () => {
-  // gr-textarea set monospace class in the ready() method.
-  // In Polymer2, ready() is called from the fixture(...) method,
-  // If ready() is called again later, some nested elements doesn't
-  // handle it correctly. A separate test-fixture is used to set
-  // properties before ready() is called.
-
-    let element;
-
-    setup(() => {
-      element = monospaceFixture.instantiate();
-    });
-
-    test('monospace is set properly', () => {
-      assert.isTrue(element.classList.contains('monospace'));
-    });
-  });
-
-  suite('gr-textarea hideBorder', () => {
-  // gr-textarea set noBorder class in the ready() method.
-  // In Polymer2, ready() is called from the fixture(...) method,
-  // If ready() is called again later, some nested elements doesn't
-  // handle it correctly. A separate test-fixture is used to set
-  // properties before ready() is called.
-
-    let element;
-
-    setup(() => {
-      element = hideBorderFixture.instantiate();
-    });
-
-    test('hideBorder is set properly', () => {
-      assert.isTrue(element.$.textarea.classList.contains('noBorder'));
-    });
-  });
-});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
new file mode 100644
index 0000000..506c348
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
@@ -0,0 +1,390 @@
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-textarea';
+import {GrTextarea} from './gr-textarea';
+import {html} from '@polymer/polymer/lib/utils/html-tag';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {CustomKeyboardEvent} from '../../../types/events';
+import {ItemSelectedEvent} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
+
+const basicFixture = fixtureFromElement('gr-textarea');
+
+const monospaceFixture = fixtureFromTemplate(html`
+  <gr-textarea monospace="true"></gr-textarea>
+`);
+
+const hideBorderFixture = fixtureFromTemplate(html`
+  <gr-textarea hide-border="true"></gr-textarea>
+`);
+
+suite('gr-textarea tests', () => {
+  let element: GrTextarea;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    sinon.stub(element.reporting, 'reportInteraction');
+  });
+
+  test('monospace is set properly', () => {
+    assert.isFalse(element.classList.contains('monospace'));
+  });
+
+  test('hideBorder is set properly', () => {
+    assert.isFalse(element.$.textarea.classList.contains('noBorder'));
+  });
+
+  test('emoji selector is not open with the textarea lacks focus', () => {
+    element.$.textarea.selectionStart = 1;
+    element.$.textarea.selectionEnd = 1;
+    element.text = ':';
+    assert.isFalse(!element.$.emojiSuggestions.isHidden);
+  });
+
+  test('emoji selector is not open when a general text is entered', () => {
+    MockInteractions.focus(element.$.textarea);
+    element.$.textarea.selectionStart = 9;
+    element.$.textarea.selectionEnd = 9;
+    element.text = 'some text';
+    assert.isFalse(!element.$.emojiSuggestions.isHidden);
+  });
+
+  test('emoji selector opens when a colon is typed & the textarea has focus', () => {
+    MockInteractions.focus(element.$.textarea);
+    // Needed for Safari tests. selectionStart is not updated when text is
+    // updated.
+    element.$.textarea.selectionStart = 1;
+    element.$.textarea.selectionEnd = 1;
+    element.text = ':';
+    flush();
+    assert.isFalse(element.$.emojiSuggestions.isHidden);
+    assert.equal(element._colonIndex, 0);
+    assert.isFalse(element._hideEmojiAutocomplete);
+    assert.equal(element._currentSearchString, '');
+  });
+
+  test('emoji selector opens when a colon is typed after space', () => {
+    MockInteractions.focus(element.$.textarea);
+    // Needed for Safari tests. selectionStart is not updated when text is
+    // updated.
+    element.$.textarea.selectionStart = 2;
+    element.$.textarea.selectionEnd = 2;
+    element.text = ' :';
+    flush();
+    assert.isFalse(element.$.emojiSuggestions.isHidden);
+    assert.equal(element._colonIndex, 1);
+    assert.isFalse(element._hideEmojiAutocomplete);
+    assert.equal(element._currentSearchString, '');
+  });
+
+  test('emoji selector doesn`t open when a colon is typed after character', () => {
+    MockInteractions.focus(element.$.textarea);
+    // Needed for Safari tests. selectionStart is not updated when text is
+    // updated.
+    element.$.textarea.selectionStart = 5;
+    element.$.textarea.selectionEnd = 5;
+    element.text = 'test:';
+    flush();
+    assert.isTrue(element.$.emojiSuggestions.isHidden);
+    assert.isTrue(element._hideEmojiAutocomplete);
+  });
+
+  test('emoji selector opens when a colon is typed and some substring', () => {
+    MockInteractions.focus(element.$.textarea);
+    // Needed for Safari tests. selectionStart is not updated when text is
+    // updated.
+    element.$.textarea.selectionStart = 1;
+    element.$.textarea.selectionEnd = 1;
+    element.text = ':';
+    element.$.textarea.selectionStart = 2;
+    element.$.textarea.selectionEnd = 2;
+    element.text = ':t';
+    flush();
+    assert.isFalse(element.$.emojiSuggestions.isHidden);
+    assert.equal(element._colonIndex, 0);
+    assert.isFalse(element._hideEmojiAutocomplete);
+    assert.equal(element._currentSearchString, 't');
+  });
+
+  test('emoji selector opens when a colon is typed in middle of text', () => {
+    MockInteractions.focus(element.$.textarea);
+    // Needed for Safari tests. selectionStart is not updated when text is
+    // updated.
+    element.$.textarea.selectionStart = 1;
+    element.$.textarea.selectionEnd = 1;
+    // Since selectionStart is on Chrome set always on end of text, we
+    // stub it to 1
+    const text = ': hello';
+    sinon.stub(element.$, 'textarea').value({
+      selectionStart: 1,
+      value: text,
+      textarea: {
+        focus: () => {},
+      },
+    });
+    element.text = text;
+    flush();
+    assert.isFalse(element.$.emojiSuggestions.isHidden);
+    assert.equal(element._colonIndex, 0);
+    assert.isFalse(element._hideEmojiAutocomplete);
+    assert.equal(element._currentSearchString, '');
+  });
+  test('emoji selector closes when text changes before the colon', () => {
+    const resetStub = sinon.stub(element, '_resetEmojiDropdown');
+    MockInteractions.focus(element.$.textarea);
+    flush();
+    element.$.textarea.selectionStart = 10;
+    element.$.textarea.selectionEnd = 10;
+    element.text = 'test test ';
+    element.$.textarea.selectionStart = 12;
+    element.$.textarea.selectionEnd = 12;
+    element.text = 'test test :';
+    element.$.textarea.selectionStart = 15;
+    element.$.textarea.selectionEnd = 15;
+    element.text = 'test test :smi';
+
+    assert.equal(element._currentSearchString, 'smi');
+    assert.isFalse(resetStub.called);
+    element.text = 'test test test :smi';
+    assert.isTrue(resetStub.called);
+  });
+
+  test('_resetEmojiDropdown', () => {
+    const closeSpy = sinon.spy(element, 'closeDropdown');
+    element._resetEmojiDropdown();
+    assert.equal(element._currentSearchString, '');
+    assert.isTrue(element._hideEmojiAutocomplete);
+    assert.equal(element._colonIndex, null);
+
+    element.$.emojiSuggestions.open();
+    flush();
+    element._resetEmojiDropdown();
+    assert.isTrue(closeSpy.called);
+  });
+
+  test('_determineSuggestions', () => {
+    const emojiText = 'tear';
+    const formatSpy = sinon.spy(element, '_formatSuggestions');
+    element._determineSuggestions(emojiText);
+    assert.isTrue(formatSpy.called);
+    assert.isTrue(
+      formatSpy.lastCall.calledWithExactly([
+        {
+          dataValue: '😂',
+          value: '😂',
+          match: "tears :')",
+          text: "😂 tears :')",
+        },
+        {dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'},
+      ])
+    );
+  });
+
+  test('_formatSuggestions', () => {
+    const matchedSuggestions = [
+      {value: '😢', match: 'tear'},
+      {value: '😂', match: 'tears'},
+    ];
+    element._formatSuggestions(matchedSuggestions);
+    assert.deepEqual(
+      [
+        {value: '😢', dataValue: '😢', match: 'tear', text: '😢 tear'},
+        {value: '😂', dataValue: '😂', match: 'tears', text: '😂 tears'},
+      ],
+      element._suggestions
+    );
+  });
+
+  test('_handleEmojiSelect', () => {
+    element.$.textarea.selectionStart = 16;
+    element.$.textarea.selectionEnd = 16;
+    element.text = 'test test :tears';
+    element._colonIndex = 10;
+    const selectedItem = ({dataset: {value: '😂'}} as unknown) as HTMLElement;
+    const event = new CustomEvent<ItemSelectedEvent>('item-selected', {
+      detail: {trigger: 'click', selected: selectedItem},
+    });
+    element._handleEmojiSelect(event);
+    assert.equal(element.text, 'test test 😂');
+  });
+
+  test('_updateCaratPosition', () => {
+    element.$.textarea.selectionStart = 4;
+    element.$.textarea.selectionEnd = 4;
+    element.text = 'test';
+    element._updateCaratPosition();
+    assert.deepEqual(
+      element.$.hiddenText.innerHTML,
+      element.text + element.$.caratSpan.outerHTML
+    );
+  });
+
+  test('newline receives matching indentation', async () => {
+    const indentCommand = sinon.stub(document, 'execCommand');
+    element.$.textarea.value = '    a';
+    element._handleEnterByKey(
+      new CustomEvent('keydown', {
+        detail: {keyboardEvent: {keyCode: 13}},
+      }) as CustomKeyboardEvent
+    );
+    await flush();
+    assert.deepEqual(indentCommand.args[0], ['insertText', false, '\n    ']);
+  });
+
+  test('ctrl+enter and meta+enter do not indent', async () => {
+    const indentCommand = sinon.stub(document, 'execCommand');
+    element.$.textarea.value = '    a';
+    element._handleEnterByKey(
+      new CustomEvent('keydown', {
+        detail: {keyboardEvent: {keyCode: 13, ctrlKey: true}},
+      }) as CustomKeyboardEvent
+    );
+    await flush();
+    assert.isTrue(indentCommand.notCalled);
+
+    element._handleEnterByKey(
+      new CustomEvent('keydown', {
+        detail: {keyboardEvent: {keyCode: 13, metaKey: true}},
+      }) as CustomKeyboardEvent
+    );
+    await flush();
+    assert.isTrue(indentCommand.notCalled);
+  });
+
+  test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
+    const resetSpy = sinon.spy(element, '_resetEmojiDropdown');
+    element.$.emojiSuggestions.dispatchEvent(
+      new CustomEvent('dropdown-closed', {
+        composed: true,
+        bubbles: true,
+      })
+    );
+    assert.isTrue(resetSpy.called);
+  });
+
+  test('_onValueChanged fires bind-value-changed', () => {
+    const listenerStub = sinon.stub();
+    const eventObject = new CustomEvent('bind-value-changed', {
+      detail: {currentTarget: {focused: false}, value: ''},
+    });
+    element.addEventListener('bind-value-changed', listenerStub);
+    element._onValueChanged(eventObject);
+    assert.isTrue(listenerStub.called);
+  });
+
+  suite('keyboard shortcuts', () => {
+    function setupDropdown() {
+      MockInteractions.focus(element.$.textarea);
+      element.$.textarea.selectionStart = 1;
+      element.$.textarea.selectionEnd = 1;
+      element.text = ':';
+      element.$.textarea.selectionStart = 1;
+      element.$.textarea.selectionEnd = 2;
+      element.text = ':1';
+      flush();
+    }
+
+    test('escape key', () => {
+      const resetSpy = sinon.spy(element, '_resetEmojiDropdown');
+      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
+      assert.isFalse(resetSpy.called);
+      setupDropdown();
+      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
+      assert.isTrue(resetSpy.called);
+      assert.isFalse(!element.$.emojiSuggestions.isHidden);
+    });
+
+    test('up key', () => {
+      const upSpy = sinon.spy(element.$.emojiSuggestions, 'cursorUp');
+      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
+      assert.isFalse(upSpy.called);
+      setupDropdown();
+      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
+      assert.isTrue(upSpy.called);
+    });
+
+    test('down key', () => {
+      const downSpy = sinon.spy(element.$.emojiSuggestions, 'cursorDown');
+      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
+      assert.isFalse(downSpy.called);
+      setupDropdown();
+      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
+      assert.isTrue(downSpy.called);
+    });
+
+    test('enter key', () => {
+      const enterSpy = sinon.spy(element.$.emojiSuggestions, 'getCursorTarget');
+      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+      assert.isFalse(enterSpy.called);
+      setupDropdown();
+      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+      assert.isTrue(enterSpy.called);
+      flush();
+      assert.equal(element.text, '💯');
+    });
+
+    test('enter key - ignored on just colon without more information', () => {
+      const enterSpy = sinon.spy(element.$.emojiSuggestions, 'getCursorTarget');
+      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+      assert.isFalse(enterSpy.called);
+      MockInteractions.focus(element.$.textarea);
+      element.$.textarea.selectionStart = 1;
+      element.$.textarea.selectionEnd = 1;
+      element.text = ':';
+      flush();
+      MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+      assert.isFalse(enterSpy.called);
+    });
+  });
+
+  suite('gr-textarea monospace', () => {
+    // gr-textarea set monospace class in the ready() method.
+    // In Polymer2, ready() is called from the fixture(...) method,
+    // If ready() is called again later, some nested elements doesn't
+    // handle it correctly. A separate test-fixture is used to set
+    // properties before ready() is called.
+
+    let element: GrTextarea;
+
+    setup(() => {
+      element = monospaceFixture.instantiate() as GrTextarea;
+    });
+
+    test('monospace is set properly', () => {
+      assert.isTrue(element.classList.contains('monospace'));
+    });
+  });
+
+  suite('gr-textarea hideBorder', () => {
+    // gr-textarea set noBorder class in the ready() method.
+    // In Polymer2, ready() is called from the fixture(...) method,
+    // If ready() is called again later, some nested elements doesn't
+    // handle it correctly. A separate test-fixture is used to set
+    // properties before ready() is called.
+
+    let element: GrTextarea;
+
+    setup(() => {
+      element = hideBorderFixture.instantiate() as GrTextarea;
+    });
+
+    test('hideBorder is set properly', () => {
+      assert.isTrue(element.$.textarea.classList.contains('noBorder'));
+    });
+  });
+});
diff --git a/proto/entities.proto b/proto/entities.proto
index 84c7fbd..d4ff736 100644
--- a/proto/entities.proto
+++ b/proto/entities.proto
@@ -35,7 +35,6 @@
 message Change {
   required Change_Id change_id = 1;
   optional Change_Key change_key = 2;
-  optional int32 row_version = 3;
   optional fixed64 created_on = 4;
   optional fixed64 last_updated_on = 5;
   optional Account_Id owner_account_id = 7;
@@ -54,6 +53,7 @@
   optional PatchSet_Id cherry_pick_of = 24;
 
   // Deleted fields, should not be reused:
+  reserved 3;    // row_version
   reserved 6;    // sortkey
   reserved 9;    // open
   reserved 11;   // nbrPatchSets