gitlab_comment_thread.ts 5.4 KB
Newer Older
1
import * as vscode from 'vscode';
2 3 4
import * as assert from 'assert';
import {
  GitLabNewService,
T
Tomas Vik 已提交
5
  GqlNote,
6
  GqlTextDiffDiscussion,
7
  GqlTextDiffNote,
8 9
  GqlTextPosition,
} from '../gitlab/gitlab_new_service';
10 11 12
import { GitLabComment } from './gitlab_comment';
import { toReviewUri } from './review_uri';

13 14 15 16 17 18
const firstNoteFrom = (discussion: GqlTextDiffDiscussion): GqlTextDiffNote => {
  const note = discussion.notes.nodes[0];
  assert(note, 'discussion should contain at least one note');
  return note;
};

T
Tomas Vik 已提交
19 20 21 22
const isDiffNote = (note: GqlNote): note is GqlTextDiffNote => {
  return Boolean(note.position && note.position.positionType === 'text');
};

23 24 25 26 27 28 29 30 31 32
const commentRangeFromPosition = (position: GqlTextPosition): vscode.Range => {
  const glLine = position.oldLine ?? position.newLine;
  const vsPosition = new vscode.Position(glLine - 1, 0); // VS Code numbers lines starting with 0, GitLab starts with 1
  return new vscode.Range(vsPosition, vsPosition);
};

const uriFromPosition = (
  position: GqlTextPosition,
  workspaceFolder: string,
  gitlabProjectId: number,
33
  mrId: number,
34
) => {
35
  const onOldVersion = position.oldLine !== null;
36 37 38 39 40 41 42
  const path = onOldVersion ? position.oldPath : position.newPath;
  const commit = onOldVersion ? position.diffRefs.baseSha : position.diffRefs.headSha;
  return toReviewUri({
    path,
    commit,
    workspacePath: workspaceFolder,
    projectId: gitlabProjectId,
43
    mrId,
44 45 46
  });
};

47 48 49
interface CreateThreadOptions {
  commentController: vscode.CommentController;
  workspaceFolder: string;
50
  mr: RestIssuable;
51
  discussion: GqlTextDiffDiscussion;
52
  gitlabService: GitLabNewService;
53 54
}

55
export class GitLabCommentThread {
56 57
  private resolved: boolean;

58 59
  private constructor(
    private vsThread: vscode.CommentThread,
60 61
    private gqlDiscussion: GqlTextDiffDiscussion,
    private gitlabService: GitLabNewService,
62
    private mr: RestIssuable,
63
  ) {
64
    this.vsThread.collapsibleState = vscode.CommentThreadCollapsibleState.Expanded;
T
Tomas Vik 已提交
65
    this.vsThread.canReply = firstNoteFrom(gqlDiscussion).userPermissions.createNote;
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
    this.resolved = gqlDiscussion.resolved;
    this.updateThreadContext();
  }

  async toggleResolved(): Promise<void> {
    await this.gitlabService.setResolved(this.gqlDiscussion.replyId, !this.resolved);
    this.resolved = !this.resolved;
    this.updateThreadContext();
  }

  private allowedToResolve(): boolean {
    const [firstNote] = this.gqlDiscussion.notes.nodes;
    assert(firstNote);
    return firstNote.userPermissions.resolveNote;
  }

82 83 84 85 86 87 88 89 90 91 92
  async deleteComment(comment: GitLabComment): Promise<void> {
    await this.gitlabService.deleteNote(comment.id);
    this.vsThread.comments = this.vsThread.comments.filter(c => {
      if (c instanceof GitLabComment) return c.id !== comment.id;
      return true;
    });
    if (this.vsThread.comments.length === 0) {
      this.dispose();
    }
  }

93 94 95 96 97 98 99 100
  startEdit(comment: GitLabComment): void {
    this.changeOneComment(comment.id, c => c.withMode(vscode.CommentMode.Editing));
  }

  cancelEdit(comment: GitLabComment): void {
    this.changeOneComment(comment.id, c => c.withMode(vscode.CommentMode.Preview).resetBody());
  }

101
  async submitEdit(comment: GitLabComment): Promise<void> {
102 103 104 105 106 107
    await this.gitlabService.updateNoteBody(
      comment.id,
      comment.body,
      comment.gqlNote.body, // this is what we think is the latest version stored in API
      this.mr,
    );
108 109 110 111 112
    this.changeOneComment(comment.id, c =>
      c.markBodyAsSubmitted().withMode(vscode.CommentMode.Preview),
    );
  }

T
Tomas Vik 已提交
113 114 115 116 117 118 119 120 121 122 123 124 125
  async reply(text: string): Promise<void> {
    const note = await this.gitlabService.createNote(this.mr, text, this.gqlDiscussion.replyId);
    assert(isDiffNote(note));
    this.vsThread.comments = [...this.vsThread.comments, GitLabComment.fromGqlNote(note, this)];
    // prevent mutating existing API response by making deeper copy
    this.gqlDiscussion = {
      ...this.gqlDiscussion,
      notes: {
        nodes: [...this.gqlDiscussion.notes.nodes, note],
      },
    };
  }

126 127 128 129 130 131 132 133 134
  private changeOneComment(id: string, changeFn: (c: GitLabComment) => GitLabComment): void {
    this.vsThread.comments = this.vsThread.comments.map(c => {
      if (c instanceof GitLabComment && c.id === id) {
        return changeFn(c);
      }
      return c;
    });
  }

135 136 137 138 139 140
  private updateThreadContext() {
    // when user doesn't have permission to resolve the discussion we don't show the
    // resolve/unresolve buttons at all (`context` stays `undefined`) because otherwise
    // user would be presented with buttons that don't do anything when clicked
    if (this.gqlDiscussion.resolvable && this.allowedToResolve()) {
      this.vsThread.contextValue = this.resolved ? 'resolved' : 'unresolved';
141
    }
142 143 144 145 146 147
  }

  dispose(): void {
    this.vsThread.dispose();
  }

148 149 150
  static createThread({
    commentController,
    workspaceFolder,
151
    mr,
152
    discussion,
153
    gitlabService,
154
  }: CreateThreadOptions): GitLabCommentThread {
155
    const { position } = firstNoteFrom(discussion);
156
    const vsThread = commentController.createCommentThread(
157
      uriFromPosition(position, workspaceFolder, mr.project_id, mr.id),
158 159 160 161 162
      commentRangeFromPosition(position),
      // the comments need to know about the thread, so we first
      // create empty thread to be able to create comments
      [],
    );
163
    const glThread = new GitLabCommentThread(vsThread, discussion, gitlabService, mr);
164 165 166
    vsThread.comments = discussion.notes.nodes.map(note =>
      GitLabComment.fromGqlNote(note, glThread),
    );
167 168 169
    return glThread;
  }
}