import * as vscode from 'vscode'; import * as assert from 'assert'; import { PROGRAMMATIC_COMMANDS } from '../../command_names'; import { ChangedFileItem } from './changed_file_item'; import { ItemModel } from './item_model'; import { GqlDiscussion, GqlTextDiffDiscussion } from '../../gitlab/graphql/get_discussions'; import { handleError } from '../../log'; import { UserFriendlyError } from '../../errors/user_friendly_error'; import { GitLabCommentThread } from '../../review/gitlab_comment_thread'; import { CommentingRangeProvider } from '../../review/commenting_range_provider'; import { WrappedRepository } from '../../git/wrapped_repository'; import { commentControllerProvider } from '../../review/comment_controller_provider'; import { GqlTextDiffNote } from '../../gitlab/graphql/shared'; import { toReviewUri } from '../../review/review_uri'; import { commentRangeFromPosition, commitFromPosition, pathFromPosition, } from '../../review/gql_position_parser'; const isTextDiffDiscussion = (discussion: GqlDiscussion): discussion is GqlTextDiffDiscussion => { const firstNote = discussion.notes.nodes[0]; return firstNote?.position?.positionType === 'text'; }; const firstNoteFrom = (discussion: GqlTextDiffDiscussion): GqlTextDiffNote => { const note = discussion.notes.nodes[0]; assert(note, 'discussion should contain at least one note'); return note; }; const uriForDiscussion = ( repository: WrappedRepository, mr: RestMr, discussion: GqlTextDiffDiscussion, ): vscode.Uri => { const { position } = firstNoteFrom(discussion); return toReviewUri({ path: pathFromPosition(position), commit: commitFromPosition(position), repositoryRoot: repository.rootFsPath, projectId: mr.project_id, mrId: mr.id, }); }; export class MrItemModel extends ItemModel { constructor(readonly mr: RestMr, readonly repository: WrappedRepository) { super(); } getTreeItem(): vscode.TreeItem { const { iid, title, author } = this.mr; const item = new vscode.TreeItem( `!${iid} ยท ${title}`, vscode.TreeItemCollapsibleState.Collapsed, ); if (author.avatar_url) { item.iconPath = vscode.Uri.parse(author.avatar_url); } item.contextValue = `mr-item-from-${this.isFromFork ? 'fork' : 'same-project'}`; return item; } private get overviewItem() { const result = new vscode.TreeItem('Overview'); result.iconPath = new vscode.ThemeIcon('note'); result.command = { command: PROGRAMMATIC_COMMANDS.SHOW_RICH_CONTENT, arguments: [this.mr, this.repository.rootFsPath], title: 'Show MR Overview', }; return result; } private async getMrDiscussions(): Promise { try { const discussions = await this.repository.getGitLabService().getDiscussions({ issuable: this.mr, }); return discussions.filter(isTextDiffDiscussion); } catch (e) { handleError( new UserFriendlyError( `The extension failed to preload discussions on the MR diff. It's possible that you've encountered https://gitlab.com/gitlab-org/gitlab/-/issues/298827.`, e, ), ); } return []; } async getChildren(): Promise { const { mrVersion } = await this.repository.reloadMr(this.mr); const discussions = await this.getMrDiscussions(); await this.addAllCommentsToVsCode(mrVersion, discussions); const allUrisWithComments = discussions.map(d => uriForDiscussion(this.repository, this.mr, d).toString(), ); const changedFiles = mrVersion.diffs.map( diff => new ChangedFileItem(this.mr, mrVersion, diff, this.repository.rootFsPath, uri => allUrisWithComments.includes(uri.toString()), ), ); return [this.overviewItem, ...changedFiles]; } private async addAllCommentsToVsCode( mrVersion: RestMrVersion, discussions: GqlTextDiffDiscussion[], ): Promise { const gitlabService = this.repository.getGitLabService(); const userCanComment = await gitlabService.canUserCommentOnMr(this.mr); const commentController = commentControllerProvider.borrowCommentController( this.mr.references.full, this.mr.title, userCanComment ? new CommentingRangeProvider(this.mr, mrVersion) : undefined, ); this.setDisposableChildren([commentController]); discussions.forEach(discussion => { const { position } = firstNoteFrom(discussion); const vsThread = commentController.createCommentThread( uriForDiscussion(this.repository, this.mr, discussion), commentRangeFromPosition(position), // the comments need to know about the thread, so we first // create empty thread to be able to create comments [], ); return new GitLabCommentThread( vsThread, discussion, this.repository.getGitLabService(), this.mr, ); }); } get isFromFork(): boolean { return this.mr.target_project_id !== this.mr.source_project_id; } }