gitlab_comment_thread.ts 4.1 KB
Newer Older
1
import * as vscode from 'vscode';
2 3 4 5 6 7
import * as assert from 'assert';
import {
  GitLabNewService,
  GqlTextDiffDiscussion,
  GqlTextPosition,
} from '../gitlab/gitlab_new_service';
8 9 10 11 12 13 14 15 16 17 18 19 20 21
import { GitLabComment } from './gitlab_comment';
import { toReviewUri } from './review_uri';

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,
) => {
22
  const onOldVersion = position.oldLine !== null;
23 24 25 26 27 28 29 30 31 32
  const path = onOldVersion ? position.oldPath : position.newPath;
  const commit = onOldVersion ? position.diffRefs.baseSha : position.diffRefs.headSha;
  return toReviewUri({
    path,
    commit,
    workspacePath: workspaceFolder,
    projectId: gitlabProjectId,
  });
};

33 34 35 36 37
interface CreateThreadOptions {
  commentController: vscode.CommentController;
  workspaceFolder: string;
  gitlabProjectId: number;
  discussion: GqlTextDiffDiscussion;
38
  gitlabService: GitLabNewService;
39 40
}

41
export class GitLabCommentThread {
42 43
  private resolved: boolean;

44 45
  private constructor(
    private vsThread: vscode.CommentThread,
46 47
    private gqlDiscussion: GqlTextDiffDiscussion,
    private gitlabService: GitLabNewService,
48
  ) {
49 50
    this.vsThread.collapsibleState = vscode.CommentThreadCollapsibleState.Expanded;
    this.vsThread.canReply = false;
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
    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;
  }

67 68 69 70 71 72 73 74 75 76 77
  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();
    }
  }

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
  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());
  }

  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;
    });
  }

95 96 97 98 99 100
  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';
101
    }
102 103 104 105 106 107
  }

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

108 109 110 111 112
  static createThread({
    commentController,
    workspaceFolder,
    gitlabProjectId,
    discussion,
113
    gitlabService,
114 115
  }: CreateThreadOptions): GitLabCommentThread {
    const { position } = discussion.notes.nodes[0];
116 117 118 119 120 121 122
    const vsThread = commentController.createCommentThread(
      uriFromPosition(position, workspaceFolder, gitlabProjectId),
      commentRangeFromPosition(position),
      // the comments need to know about the thread, so we first
      // create empty thread to be able to create comments
      [],
    );
123
    const glThread = new GitLabCommentThread(vsThread, discussion, gitlabService);
124 125 126
    vsThread.comments = discussion.notes.nodes.map(note =>
      GitLabComment.fromGqlNote(note, glThread),
    );
127 128 129
    return glThread;
  }
}