Optimize getting Commented Code

To calculate diff for user suggested edit we need to get
code that is inside comment range.

- request only if comment has user suggestion
- reuse commented code for apply fix dialog

Release-Notes: skip
Google-Bug-Id: b/298011591
Change-Id: I935e338ac6adfc287e8ff4904d7c0b1ad558f641
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts b/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts
index 974329a..46b43c9 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts
@@ -10,16 +10,26 @@
 import {isDefined} from '../../../types/types';
 import {select} from '../../../utils/observable-util';
 import {Comment} from '../../../types/common';
+import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
+import {NumericChangeId, isBase64FileContent} from '../../../api/rest-api';
+import {assert, assertIsDefined} from '../../../utils/common-util';
+import {getContentInCommentRange} from '../../../utils/comment-util';
+import {SpecialFilePath} from '../../../constants/constants';
 
 export interface CommentState {
-  comment: Comment;
+  comment?: Comment;
   commentedText?: string;
 }
 
+const initialState: CommentState = {
+  comment: undefined,
+  commentedText: undefined,
+};
+
 export const commentModelToken = define<CommentModel>('diff-model');
 
 export class CommentModel extends Model<CommentState | undefined> {
-  readonly comment$: Observable<Comment> = select(
+  readonly comment$: Observable<Comment | undefined> = select(
     this.state$.pipe(filter(isDefined)),
     commentState => commentState.comment
   );
@@ -28,4 +38,32 @@
     this.state$.pipe(filter(isDefined)),
     commentState => commentState.commentedText
   );
+
+  constructor(private readonly restApiService: RestApiService) {
+    super(initialState);
+  }
+
+  async getCommentedCode(
+    comment?: Comment,
+    changeNum?: NumericChangeId
+  ): Promise<string | undefined> {
+    assertIsDefined(comment, 'comment');
+    assertIsDefined(changeNum, 'changeNum');
+    if (comment.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) return;
+    const file = await this.restApiService.getFileContent(
+      changeNum,
+      comment.path!,
+      comment.patch_set!
+    );
+    assert(
+      !!file && isBase64FileContent(file) && !!file.content,
+      'file content for comment not found'
+    );
+    const commentedText = getContentInCommentRange(file.content, comment);
+    assert(!!commentedText, 'file content for comment not found');
+    this.updateState({
+      commentedText,
+    });
+    return commentedText;
+  }
 }
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 6ca4702..0a5005a 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -214,6 +214,9 @@
   @state()
   isOwner = false;
 
+  @state()
+  commentedText?: string;
+
   private readonly restApiService = getAppContext().restApiService;
 
   private readonly reporting = getAppContext().reportingService;
@@ -230,7 +233,7 @@
 
   private readonly shortcuts = new ShortcutController(this);
 
-  private commentModel = new CommentModel(undefined);
+  private commentModel = new CommentModel(this.restApiService);
 
   /**
    * This is triggered when the user types into the editing textarea. We then
@@ -995,14 +998,15 @@
           KnownExperimentId.DIFF_FOR_USER_SUGGESTED_EDIT
         ) ||
         !this.changeNum ||
-        !this.comment
+        !this.comment ||
+        !hasUserSuggestion(this.comment)
       )
         return;
       (async () => {
-        const commentedText = await this.getCommentedCode();
-        this.commentModel.updateState({
-          commentedText,
-        });
+        this.commentedText = await this.commentModel.getCommentedCode(
+          this.comment,
+          this.changeNum
+        );
       })();
     }
   }
@@ -1072,12 +1076,15 @@
     if (hasUserSuggestion(this.comment) || replacement) {
       replacement = replacement ?? getUserSuggestion(this.comment);
       assert(!!replacement, 'malformed user suggestion');
-      const line = await this.getCommentedCode();
+      let commentedCode = this.commentedText;
+      if (!commentedCode) {
+        commentedCode = await this.getCommentedCode();
+      }
 
       return {
         fixSuggestions: createUserFixSuggestion(
           this.comment,
-          line,
+          commentedCode,
           replacement
         ),
         patchNum: this.comment.patch_set,
@@ -1196,11 +1203,10 @@
     }${USER_SUGGESTION_START_PATTERN}${line}${'\n```'}`;
   }
 
+  // TODO(milutin): Remove once feature flag is rollout and use only model
   async getCommentedCode() {
     assertIsDefined(this.comment, 'comment');
     assertIsDefined(this.changeNum, 'changeNum');
-    // TODO(milutin): Show a toast while the file is being loaded.
-    // TODO(milutin): This should be moved into a service/model.
     const file = await this.restApiService.getFileContent(
       this.changeNum,
       this.comment.path!,
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index 88866d4..a287659 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -41,7 +41,10 @@
       testResolver(changeModelToken),
       getAppContext().restApiService
     );
-    const commentModel = new CommentModel({comment: createComment()});
+    const commentModel = new CommentModel(getAppContext().restApiService);
+    commentModel.updateState({
+      comment: createComment(),
+    });
     await setCommentLinks({
       customLinkRewrite: {
         match: '(LinkRewriteMe)',
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
index 423fe62..35b2d90 100644
--- a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
@@ -13,12 +13,16 @@
 } from '../gr-comment-model/gr-comment-model';
 import {wrapInProvider} from '../../../models/di-provider-element';
 import {createComment} from '../../../test/test-data-generators';
+import {getAppContext} from '../../../services/app-context';
 
 suite('gr-user-suggestion-fix tests', () => {
   let element: GrUserSuggestionsFix;
 
   setup(async () => {
-    const commentModel = new CommentModel({comment: createComment()});
+    const commentModel = new CommentModel(getAppContext().restApiService);
+    commentModel.updateState({
+      comment: createComment(),
+    });
     element = (
       await fixture<GrUserSuggestionsFix>(
         wrapInProvider(