Merge "Add basic caching for comment autocompletion"
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index b1b8bf2..c27ca5b 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -894,23 +894,50 @@
         .placeholder=${this.messagePlaceholder}
         text=${this.messageText}
         autocompleteHint=${this.autocompleteHint}
-        @text-changed=${(e: ValueChangedEvent) => {
-          // TODO: This is causing a re-render of <gr-comment> on every key
-          // press. Try to avoid always setting `this.messageText` or at least
-          // debounce it. Most of the code can just inspect the current value
-          // of the textare instead of needing a dedicated property.
-          this.messageText = e.detail.value;
-          // As soon as the user changes the next the hint for autocompletion
-          // is invalidated.
-          this.autocompleteHint = '';
-          this.autoSaveTrigger$.next();
-          this.generateSuggestionTrigger$.next();
-          this.autocompleteTrigger$.next();
-        }}
+        @text-changed=${this.handleTextChanged}
       ></gr-suggestion-textarea>
     `;
   }
 
+  private handleTextChanged(e: ValueChangedEvent) {
+    const oldValue = this.messageText;
+    const newValue = e.detail.value;
+    if (oldValue === newValue) return;
+    // TODO: This is causing a re-render of <gr-comment> on every key
+    // press. Try to avoid always setting `this.messageText` or at least
+    // debounce it. Most of the code can just inspect the current value
+    // of the textare instead of needing a dedicated property.
+    this.messageText = newValue;
+
+    this.handleTextChangedForAutocomplete(oldValue, newValue);
+    this.autoSaveTrigger$.next();
+    this.generateSuggestionTrigger$.next();
+  }
+
+  // visible for testing
+  handleTextChangedForAutocomplete(oldValue: string, newValue: string) {
+    if (oldValue === newValue) return;
+    // As soon as the user changes the text the hint for autocompletion
+    // is invalidated, *if* what the user typed does not match the
+    // autocompletion!
+    const charsAdded = newValue.length - oldValue.length;
+    if (
+      charsAdded > 0 &&
+      newValue.startsWith(oldValue) &&
+      this.autocompleteHint.startsWith(newValue.substring(oldValue.length))
+    ) {
+      // What the user typed matches the hint, so we keep the hint, but shorten
+      // it accordingly.
+      this.autocompleteHint = this.autocompleteHint.substring(charsAdded);
+      return;
+    }
+
+    // The default behavior is to reset the hint and to generate a new
+    // autocomplete suggestion.
+    this.autocompleteHint = '';
+    this.autocompleteTrigger$.next();
+  }
+
   private renderCommentMessage() {
     if (this.collapsed || this.editing) return;
 
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 74e133b..7b4f63a 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -896,6 +896,32 @@
     });
   });
 
+  suite('handleTextChangedForAutocomplete', () => {
+    test('foo -> foo with asdf', async () => {
+      element.autocompleteHint = 'asdf';
+      element.handleTextChangedForAutocomplete('foo', 'foo');
+      assert.equal(element.autocompleteHint, 'asdf');
+    });
+
+    test('foo -> bar with asdf', async () => {
+      element.autocompleteHint = 'asdf';
+      element.handleTextChangedForAutocomplete('foo', 'bar');
+      assert.equal(element.autocompleteHint, '');
+    });
+
+    test('foo -> foofoo with asdf', async () => {
+      element.autocompleteHint = 'asdf';
+      element.handleTextChangedForAutocomplete('foo', 'foofoo');
+      assert.equal(element.autocompleteHint, '');
+    });
+
+    test('foo -> foofoo with foomore', async () => {
+      element.autocompleteHint = 'foomore';
+      element.handleTextChangedForAutocomplete('foo', 'foofoo');
+      assert.equal(element.autocompleteHint, 'more');
+    });
+  });
+
   suite('suggest edit', () => {
     let element: GrComment;
     setup(async () => {