Merge "Add a new plugin API for comment autocompletion"
diff --git a/polygerrit-ui/app/api/suggestions.ts b/polygerrit-ui/app/api/suggestions.ts
index b4158be..c3089a9 100644
--- a/polygerrit-ui/app/api/suggestions.ts
+++ b/polygerrit-ui/app/api/suggestions.ts
@@ -27,7 +27,21 @@
lineNumber?: number;
}
+export declare interface AutocompleteCommentRequest {
+ id: string;
+ commentText: string;
+ changeInfo: ChangeInfo;
+ patchsetNumber: RevisionPatchSetNum;
+ filePath: string;
+ range?: CommentRange;
+ lineNumber?: number;
+}
+
export declare interface SuggestionsProvider {
+ autocompleteComment?(
+ req: AutocompleteCommentRequest
+ ): Promise<AutocompleteCommentResponse>;
+
/**
* Gerrit calls these methods when ...
* - ... user types a comment draft
@@ -55,6 +69,11 @@
supportedFileExtensions?: string[];
}
+export declare interface AutocompleteCommentResponse {
+ responseCode: ResponseCode;
+ completion?: string;
+}
+
export declare interface SuggestCodeResponse {
responseCode: ResponseCode;
suggestions: Suggestion[];
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 3509146..254845e 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -93,6 +93,7 @@
// visible for testing
export const AUTO_SAVE_DEBOUNCE_DELAY_MS = 2000;
export const GENERATE_SUGGESTION_DEBOUNCE_DELAY_MS = 500;
+export const AUTOCOMPLETE_DEBOUNCE_DELAY_MS = 200;
export const ENABLE_GENERATE_SUGGESTION_STORAGE_KEY =
'enableGenerateSuggestionStorageKeyForCommentWithId-';
@@ -219,6 +220,12 @@
@state()
messageText = '';
+ /**
+ * An hint for autocompleting the comment message from plugin suggestion
+ * providers.
+ */
+ @state() autocompleteHint = '';
+
/* The 'dirty' state of !comment.unresolved, which will be saved on demand. */
@state()
unresolved = true;
@@ -300,6 +307,12 @@
private generateSuggestionTrigger$ = new Subject();
/**
+ * This is triggered when the user types into the editing textarea. We then
+ * debounce it and call autocompleteComment().
+ */
+ private autocompleteTrigger$ = new Subject();
+
+ /**
* Set to the content of DraftInfo when entering editing mode.
* Only used for "Cancel".
*/
@@ -392,6 +405,23 @@
() => this.getConfigModel().docsBaseUrl$,
docsBaseUrl => (this.docsBaseUrl = docsBaseUrl)
);
+ subscribe(
+ this,
+ () => this.getPluginLoader().pluginsModel.suggestionsPlugins$,
+ // We currently support results from only 1 provider.
+ suggestionsPlugins =>
+ (this.suggestionsProvider = suggestionsPlugins?.[0]?.provider)
+ );
+ subscribe(
+ this,
+ () =>
+ this.autocompleteTrigger$.pipe(
+ debounceTime(AUTOCOMPLETE_DEBOUNCE_DELAY_MS)
+ ),
+ () => {
+ this.autocompleteComment();
+ }
+ );
if (
this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT) ||
this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2)
@@ -424,15 +454,6 @@
override connectedCallback() {
super.connectedCallback();
- this.getPluginLoader()
- .awaitPluginsLoaded()
- .then(() => {
- const suggestionsPlugins =
- this.getPluginLoader().pluginsModel.getState().suggestionsPlugins;
- // We currently support results from only 1 provider.
- this.suggestionsProvider = suggestionsPlugins?.[0]?.provider;
- });
-
if (this.comment?.id) {
const generateSuggestionStoredContent =
this.getStorage().getEditableContentItem(
@@ -872,14 +893,19 @@
rows="4"
.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();
}}
></gr-suggestion-textarea>
`;
@@ -1286,6 +1312,39 @@
}
}
+ private async autocompleteComment() {
+ const enabled = this.flagsService.isEnabled(
+ KnownExperimentId.COMMENT_AUTOCOMPLETION
+ );
+ const suggestionsProvider = this.suggestionsProvider;
+ const change = this.getChangeModel().getChange();
+ if (
+ !enabled ||
+ !suggestionsProvider?.autocompleteComment ||
+ !change ||
+ !this.comment?.patch_set ||
+ !this.comment.path ||
+ this.messageText.length === 0
+ ) {
+ return;
+ }
+ const commentText = this.messageText;
+ const response = await suggestionsProvider.autocompleteComment({
+ id: id(this.comment),
+ commentText,
+ changeInfo: change as ChangeInfo,
+ patchsetNumber: this.comment?.patch_set,
+ filePath: this.comment.path,
+ range: this.comment.range,
+ lineNumber: this.comment.line,
+ });
+ // If between request and response the user has changed the message, then
+ // ignore the suggestion for the old message text.
+ if (this.messageText !== commentText) return;
+ if (!response?.completion) return;
+ this.autocompleteHint = response.completion;
+ }
+
private renderRobotActions() {
if (!this.account || !isRobot(this.comment)) return;
const endpoint = html`
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 701052d..b748544 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
@@ -408,6 +408,7 @@
<div class="body">
<gr-suggestion-textarea
autocomplete="on"
+ autocompletehint=""
class="code editMessage"
code=""
id="editTextarea"
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-textarea/gr-suggestion-textarea.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-textarea/gr-suggestion-textarea.ts
index 04f66a7..2d22f6a 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-textarea/gr-suggestion-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-textarea/gr-suggestion-textarea.ts
@@ -143,6 +143,7 @@
.placeholder=${this.el.placeholder}
?disabled=${this.el.disabled}
.value=${this.el.text}
+ .hint=${this.el.autocompleteHint}
@input=${(e: InputEvent) => {
const value = (e.target as GrTextarea).value;
this.el.text = value ?? '';
@@ -227,6 +228,12 @@
standard monospace font. */
@property({type: Boolean}) code = false;
+ /**
+ * An autocompletion hint that is passed to <gr-textarea>, which will allow\
+ * the user to accept it by pressing tab.
+ */
+ @property({type: String}) autocompleteHint = '';
+
@state() suggestions: (Item | EmojiSuggestion)[] = [];
// Accessed in tests.
diff --git a/polygerrit-ui/app/models/plugins/plugins-model.ts b/polygerrit-ui/app/models/plugins/plugins-model.ts
index 8e9bead..880bcd0 100644
--- a/polygerrit-ui/app/models/plugins/plugins-model.ts
+++ b/polygerrit-ui/app/models/plugins/plugins-model.ts
@@ -89,6 +89,11 @@
public coveragePlugins$ = select(this.state$, state => state.coveragePlugins);
+ public suggestionsPlugins$ = select(
+ this.state$,
+ state => state.suggestionsPlugins
+ );
+
public pluginsLoaded$ = select(this.state$, state => state.pluginsLoaded);
constructor() {