diff --git a/package.json b/package.json index f93b0d7487..92aae28050 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "version": "0.3.2", "publisher": "GitHub", "engines": { - "vscode": "^1.30.0" + "vscode": "^1.31.0" }, "categories": [ "Other" diff --git a/preview-src/index.css b/preview-src/index.css index ba1cf3f69b..2d567e10de 100644 --- a/preview-src/index.css +++ b/preview-src/index.css @@ -108,6 +108,11 @@ body .comment-container .review-comment-header a { margin-left: auto; } +.review-comment-header .pending { + color: inherit; + font-style: italic; +} + .comment-actions button { background-color: transparent; padding: 2px 6px 3px; diff --git a/preview-src/pullRequestOverviewRenderer.ts b/preview-src/pullRequestOverviewRenderer.ts index 40047a00f0..e0904c28a9 100644 --- a/preview-src/pullRequestOverviewRenderer.ts +++ b/preview-src/pullRequestOverviewRenderer.ts @@ -341,22 +341,33 @@ class CommentNode { authorLink.href = this._comment.user.html_url; authorLink.textContent = this._comment.user.login; - const timestamp: HTMLAnchorElement = document.createElement('a'); - timestamp.className = 'timestamp'; - timestamp.href = this._comment.html_url; - timestamp.textContent = dateFromNow(this._comment.created_at); + commentHeader.appendChild(authorLink); - const commentState = document.createElement('span'); - commentState.textContent = 'commented'; + const isPending = this._review && this._review.isPending(); + if (isPending) { + const pendingTag = document.createElement('a'); + pendingTag.className = 'pending'; + pendingTag.href = this._comment.html_url; + pendingTag.textContent = 'Pending'; + + commentHeader.appendChild(pendingTag); + } else { + const timestamp: HTMLAnchorElement = document.createElement('a'); + timestamp.className = 'timestamp'; + timestamp.href = this._comment.html_url; + timestamp.textContent = dateFromNow(this._comment.created_at); + + const commentState = document.createElement('span'); + commentState.textContent = 'commented'; + + commentHeader.appendChild(commentState); + commentHeader.appendChild(timestamp); + } this._commentBody.className = 'comment-body'; this._commentBody.innerHTML = md.render(emoji.emojify(this._comment.body)); - commentHeader.appendChild(authorLink); - commentHeader.appendChild(commentState); - commentHeader.appendChild(timestamp); - if (this._comment.canEdit || this._comment.canDelete) { this._actionsBar = new ActionsBar(this._commentContainer, this._comment as Comment, this._commentBody, this._messageHandler, (e) => { }, 'pr.edit-comment', 'pr.delete-comment', this._review); const actionBarElement = this._actionsBar.render(); @@ -471,6 +482,10 @@ class ReviewNode { constructor(private _review: ReviewEvent, private _messageHandler: MessageHandler) { } + isPending(): boolean { + return this._review.state === 'pending'; + } + deleteCommentFromReview(comment: Comment): void { const deletedCommentIndex = this._review.comments.findIndex(c => c.id.toString() === comment.id.toString()); this._review.comments.splice(deletedCommentIndex, 1); @@ -497,9 +512,6 @@ class ReviewNode { render(): HTMLElement | undefined { // Ignore pending or empty reviews const isEmpty = !this._review.body && !(this._review.comments && this._review.comments.length); - if (this._review.state === 'pending') { - return undefined; - } this._commentContainer = document.createElement('div'); this._commentContainer.classList.add('comment-container', 'comment'); @@ -530,7 +542,12 @@ class ReviewNode { const timestamp: HTMLAnchorElement = document.createElement('a'); timestamp.className = 'timestamp'; timestamp.href = this._review.html_url; - timestamp.textContent = dateFromNow(this._review.submitted_at); + const isPending = this.isPending(); + timestamp.textContent = isPending ? 'Pending' : dateFromNow(this._review.submitted_at); + + if (isPending) { + timestamp.classList.add('pending'); + } commentHeader.appendChild(userLogin); commentHeader.appendChild(reviewState); diff --git a/src/common/comment.ts b/src/common/comment.ts index 2d6f960a92..cd11b0f062 100644 --- a/src/common/comment.ts +++ b/src/common/comment.ts @@ -11,4 +11,5 @@ export interface Comment extends Github.PullRequestsCreateCommentResponse { diff_hunks?: DiffHunk[]; canEdit?: boolean; canDelete?: boolean; + isDraft?: boolean; } diff --git a/src/github/githubRepository.ts b/src/github/githubRepository.ts index 8c4c91df7f..23f5780373 100644 --- a/src/github/githubRepository.ts +++ b/src/github/githubRepository.ts @@ -187,7 +187,8 @@ export class GitHubRepository implements IGitHubRepository { created_at, updated_at, head, - base + base, + node_id }) => { if (!head.repo) { Logger.appendLine( @@ -211,7 +212,8 @@ export class GitHubRepository implements IGitHubRepository { comments: 0, commits: 0, head, - base + base, + node_id }; return new PullRequestModel(this, this.remote, item); diff --git a/src/github/interface.ts b/src/github/interface.ts index 61141dd47a..41db907a53 100644 --- a/src/github/interface.ts +++ b/src/github/interface.ts @@ -67,6 +67,7 @@ export type PullRequest = Pick< | 'commits' | 'head' | 'base' + | 'node_id' >; export interface IRawFileChange { @@ -133,11 +134,10 @@ export interface IPullRequestManager { getPullRequestComments(pullRequest: IPullRequestModel): Promise; getPullRequestCommits(pullRequest: IPullRequestModel): Promise; getCommitChangedFiles(pullRequest: IPullRequestModel, commit: Github.PullRequestsGetCommitsResponseItem): Promise; - getReviewComments(pullRequest: IPullRequestModel, reviewId: number): Promise; getTimelineEvents(pullRequest: IPullRequestModel): Promise; getIssueComments(pullRequest: IPullRequestModel): Promise; createIssueComment(pullRequest: IPullRequestModel, text: string): Promise; - createCommentReply(pullRequest: IPullRequestModel, body: string, reply_to: string): Promise; + createCommentReply(pullRequest: IPullRequestModel, body: string, reply_to: Comment): Promise; createComment(pullRequest: IPullRequestModel, body: string, path: string, position: number): Promise; getPullRequestDefaults(): Promise; createPullRequest(params: PullRequestsCreateParams): Promise; @@ -154,6 +154,10 @@ export interface IPullRequestManager { getPullRequestFileChangesInfo(pullRequest: IPullRequestModel): Promise; getPullRequestRepositoryDefaultBranch(pullRequest: IPullRequestModel): Promise; getStatusChecks(pullRequest: IPullRequestModel): Promise; + inDraftMode(pullRequest: IPullRequestModel): Promise; + deleteReview(pullRequest: IPullRequestModel): Promise; + submitReview(pullRequest: IPullRequestModel): Promise; + startReview(pullRequest: IPullRequestModel): Promise; /** * Fullfill information for a pull request which we can't fetch with one single api call. diff --git a/src/github/pullRequestManager.ts b/src/github/pullRequestManager.ts index 53ac3a8d1d..4ee598f5b1 100644 --- a/src/github/pullRequestManager.ts +++ b/src/github/pullRequestManager.ts @@ -19,6 +19,8 @@ import { formatError, uniqBy, Predicate, groupBy } from '../common/utils'; import { Repository, RefType, UpstreamRef, Branch } from '../typings/git'; import Logger from '../common/logger'; +const queries = require('./queries.gql'); + interface PageInformation { pullRequestPage: number; hasMorePages: boolean; @@ -63,7 +65,7 @@ export class BadUpstreamError extends Error { } get message() { - const {upstreamRef: {remote, name}, branchName, problem} = this; + const { upstreamRef: { remote, name }, branchName, problem } = this; return `The upstream ref ${remote}/${name} for branch ${branchName} ${problem}.`; } } @@ -76,6 +78,15 @@ const enum IncludeRemote { All } +interface NewCommentPosition { + path: string; + position: number; +} + +interface ReplyCommentPosition { + inReplyTo: string; +} + export class PullRequestManager implements IPullRequestManager { static ID = 'PullRequestManager'; private _activePullRequest?: IPullRequestModel; @@ -352,9 +363,39 @@ export class PullRequestManager implements IPullRequestManager { } async getPullRequestComments(pullRequest: IPullRequestModel): Promise { + const { supportsGraphQl } = (pullRequest as PullRequestModel).githubRepository; + return supportsGraphQl + ? this.getAllPullRequestReviewComments(pullRequest) + : this.getPullRequestReviewComments(pullRequest); + } + + private async getAllPullRequestReviewComments(pullRequest: IPullRequestModel): Promise { + const { remote, query } = await (pullRequest as PullRequestModel).githubRepository.ensure(); + try { + const { data } = await query({ + query: queries.PullRequestComments, + variables: { + owner: remote.owner, + name: remote.repositoryName, + number: pullRequest.prNumber, + } + }); + + const comments = data.repository.pullRequest.reviews.nodes + .map(node => node.comments.nodes.map(comment => this.addCommentPermissions(toComment(comment), remote))) + .reduce((prev, curr) => curr = prev.concat(curr), []); + return parserCommentDiffHunk(comments); + } catch (e) { + Logger.appendLine(`Failed to get pull request review comments: ${formatError(e)}`); + } + } + + /** + * Returns review comments from the pull request using the REST API, comments on pending reviews are not included. + */ + private async getPullRequestReviewComments(pullRequest: IPullRequestModel): Promise { Logger.debug(`Fetch comments of PR #${pullRequest.prNumber} - enter`, PullRequestManager.ID); const { remote, octokit } = await (pullRequest as PullRequestModel).githubRepository.ensure(); - const reviewData = await octokit.pullRequests.getComments({ owner: remote.owner, repo: remote.repositoryName, @@ -386,7 +427,7 @@ export class PullRequestManager implements IPullRequestManager { async getCommitChangedFiles(pullRequest: IPullRequestModel, commit: Github.PullRequestsGetCommitsResponseItem): Promise { try { - Logger.debug(`Fetch file changes of commit ${commit.sha} in PR #${pullRequest.prNumber} - enter`, PullRequestManager.ID); + Logger.debug(`Fetch file changes of commit ${commit.sha} in PR #${pullRequest.prNumber} - enter`, PullRequestManager.ID); const { octokit, remote } = await (pullRequest as PullRequestModel).githubRepository.ensure(); const fullCommit = await octokit.repos.getCommit({ owner: remote.owner, @@ -402,22 +443,6 @@ export class PullRequestManager implements IPullRequestManager { } } - async getReviewComments(pullRequest: IPullRequestModel, reviewId: number): Promise { - Logger.debug(`Fetch comments of review #${reviewId} in PR #${pullRequest.prNumber} - enter`, PullRequestManager.ID); - const { octokit, remote } = await (pullRequest as PullRequestModel).githubRepository.ensure(); - - const reviewData = await octokit.pullRequests.getReviewComments({ - owner: remote.owner, - repo: remote.repositoryName, - number: pullRequest.prNumber, - review_id: reviewId - }); - - Logger.debug(`Fetch comments of review #${reviewId} in PR #${pullRequest.prNumber} - `, PullRequestManager.ID); - const rawComments = reviewData.data.map(comment => this.addCommentPermissions(comment, remote)); - return parserCommentDiffHunk(rawComments); - } - async getTimelineEvents(pullRequest: IPullRequestModel): Promise { Logger.debug(`Fetch timeline events of PR #${pullRequest.prNumber} - enter`, PullRequestManager.ID); const { octokit, remote } = await (pullRequest as PullRequestModel).githubRepository.ensure(); @@ -461,7 +486,12 @@ export class PullRequestManager implements IPullRequestManager { return this.addCommentPermissions(promise.data as Comment, remote); } - async createCommentReply(pullRequest: IPullRequestModel, body: string, reply_to: string): Promise { + async createCommentReply(pullRequest: IPullRequestModel, body: string, reply_to: Comment): Promise { + const pendingReviewId = await this.getPendingReviewId(pullRequest as PullRequestModel); + if (pendingReviewId) { + return this.addCommentToPendingReview(pullRequest as PullRequestModel, pendingReviewId, body, { inReplyTo: reply_to.node_id }); + } + const { octokit, remote } = await (pullRequest as PullRequestModel).githubRepository.ensure(); try { @@ -470,7 +500,7 @@ export class PullRequestManager implements IPullRequestManager { repo: remote.repositoryName, number: pullRequest.prNumber, body: body, - in_reply_to: Number(reply_to) + in_reply_to: Number(reply_to.id) }); return this.addCommentPermissions(ret.data, remote); @@ -479,7 +509,82 @@ export class PullRequestManager implements IPullRequestManager { } } + async deleteReview(pullRequest: PullRequestModel): Promise { + const pendingReviewId = await this.getPendingReviewId(pullRequest as PullRequestModel); + const { mutate } = await pullRequest.githubRepository.ensure(); + const { data } = await mutate({ + mutation: queries.DeleteReview, + variables: { + input: { pullRequestReviewId: pendingReviewId } + } + }); + + return data.deletePullRequestReview.pullRequestReview.comments.nodes.map(toComment); + } + + async startReview(pullRequest: PullRequestModel): Promise { + const { mutate } = await (pullRequest as PullRequestModel).githubRepository.ensure(); + return mutate({ + mutation: queries.StartReview, + variables: { + input: { + body: '', + pullRequestId: pullRequest.prItem.node_id + } + } + }).then(x => x.data).catch(e => { + Logger.appendLine(`Failed to start review: ${e.message}`); + }); + } + + async inDraftMode(pullRequest: IPullRequestModel): Promise { + return !!await this.getPendingReviewId(pullRequest as PullRequestModel); + } + + async getPendingReviewId(pullRequest = this._activePullRequest as PullRequestModel): Promise { + if (!pullRequest.githubRepository.supportsGraphQl()) { + return null; + } + + const { query, octokit } = await pullRequest.githubRepository.ensure(); + const { currentUser = '' } = octokit as any; + try { + const { data } = await query({ + query: queries.GetPendingReviewId, + variables: { + pullRequestId: (pullRequest as PullRequestModel).prItem.node_id, + author: currentUser.login + } + }); + return data.node.reviews.nodes[0].id; + } catch (error) { + return null; + } + } + + async addCommentToPendingReview(pullRequest: PullRequestModel, reviewId: string, body: string, position: NewCommentPosition | ReplyCommentPosition): Promise { + const { mutate, remote } = await pullRequest.githubRepository.ensure(); + const { data } = await mutate({ + mutation: queries.AddComment, + variables: { + input: { + pullRequestReviewId: reviewId, + body, + ...position + } + } + }); + + const { comment } = data.addPullRequestReviewComment; + return this.addCommentPermissions(toComment(comment), remote); + } + async createComment(pullRequest: IPullRequestModel, body: string, path: string, position: number): Promise { + const pendingReviewId = await this.getPendingReviewId(pullRequest as PullRequestModel); + if (pendingReviewId) { + return this.addCommentToPendingReview(pullRequest as PullRequestModel, pendingReviewId, body, { path, position }); + } + const { octokit, remote } = await (pullRequest as PullRequestModel).githubRepository.ensure(); try { @@ -503,13 +608,13 @@ export class PullRequestManager implements IPullRequestManager { if (!this.repository.state.HEAD) { throw new DetachedHeadError(this.repository); } - const {origin} = this; + const { origin } = this; const meta = await origin.getMetadata(); const parent = meta.fork ? meta.parent : await (this.findRepo(byRemoteName('upstream')) || origin).getMetadata(); const branchName = this.repository.state.HEAD.name; - const {title, body} = titleAndBodyFrom(await this.getHeadCommitMessage()); + const { title, body } = titleAndBodyFrom(await this.getHeadCommitMessage()); return { title, body, owner: parent.owner.login, @@ -525,8 +630,8 @@ export class PullRequestManager implements IPullRequestManager { } async getHeadCommitMessage(): Promise { - const {repository} = this; - const {message} = await repository.getCommit(repository.state.HEAD.commit); + const { repository } = this; + const { message } = await repository.getCommit(repository.state.HEAD.commit); return message; } @@ -535,7 +640,7 @@ export class PullRequestManager implements IPullRequestManager { throw new NoGitHubReposError(this.repository); } - const {upstreamRef} = this; + const { upstreamRef } = this; if (upstreamRef) { // If our current branch has an upstream ref set, find its GitHubRepository. const upstream = this.findRepo(byRemoteName(upstreamRef.remote)); @@ -566,7 +671,7 @@ export class PullRequestManager implements IPullRequestManager { } get upstreamRef(): UpstreamRef | undefined { - const {HEAD} = this.repository.state; + const { HEAD } = this.repository.state; return HEAD && HEAD.upstream; } @@ -606,7 +711,8 @@ export class PullRequestManager implements IPullRequestManager { comments: 0, commits: 0, head: data.head, - base: data.base + base: data.base, + node_id: data.node_id }; const pullRequestModel = new PullRequestModel(repo, repo.remote, item); @@ -769,6 +875,24 @@ export class PullRequestManager implements IPullRequestManager { return ret.data; } + public async submitReview(pullRequest: IPullRequestModel): Promise { + const pendingReviewId = await this.getPendingReviewId(pullRequest as PullRequestModel); + const { mutate } = await (pullRequest as PullRequestModel).githubRepository.ensure(); + + if (pendingReviewId) { + const { data } = await mutate({ + mutation: queries.SubmitReview, + variables: { + id: pendingReviewId + } + }); + + return data.submitPullRequestReview.pullRequestReview.comments.nodes.map(toComment); + } else { + Logger.appendLine(`Submitting review failed, no pending review for current pull request: ${pullRequest.prNumber}.`); + } + } + async requestChanges(pullRequest: IPullRequestModel, message?: string): Promise { return this.createReview(pullRequest, ReviewEvent.RequestChanges, message) .then(x => { @@ -1007,14 +1131,14 @@ export function getEventType(text: string) { } const ownedByMe: Predicate = repo => { - const { currentUser=null } = repo.octokit as any; + const { currentUser = null } = repo.octokit as any; return currentUser && repo.remote.owner === currentUser.login; }; const byRemoteName = (name: string): Predicate => - ({remote: {remoteName}}) => remoteName === name; + ({ remote: { remoteName } }) => remoteName === name; -const titleAndBodyFrom = (message: string): {title: string, body: string} => { +const titleAndBodyFrom = (message: string): { title: string, body: string } => { const idxLineBreak = message.indexOf('\n'); return { title: idxLineBreak === -1 @@ -1026,3 +1150,20 @@ const titleAndBodyFrom = (message: string): {title: string, body: string} => { : message.slice(idxLineBreak + 1), }; }; + +const toComment = (comment: any): any => ({ + id: comment.databaseId, + node_id: comment.id, + body: comment.body, + user: { + login: comment.author.login, + avatar_url: comment.author.avatarUrl, + }, + position: comment.position, + url: comment.url, + path: comment.path, + original_position: comment.originalPosition, + diff_hunk: comment.diffHunk, + isDraft: comment.state === 'PENDING', + pull_request_review_id: comment.pullRequestReview && comment.pullRequestReview.databaseId +}); \ No newline at end of file diff --git a/src/github/queries.gql b/src/github/queries.gql new file mode 100644 index 0000000000..df31e4f366 --- /dev/null +++ b/src/github/queries.gql @@ -0,0 +1,74 @@ +# /*--------------------------------------------------------------------------------------------- +# * Copyright (c) Microsoft Corporation. All rights reserved. +# * Licensed under the MIT License. See License.txt in the project root for license information. +# *--------------------------------------------------------------------------------------------*/ + +fragment Comment on PullRequestReviewComment { + id databaseId url + author { login avatarUrl } + path originalPosition + body + diffHunk + position + state + pullRequestReview { databaseId } +} + +query GetPendingReviewId($pullRequestId: ID!, $author: String!) { + node(id: $pullRequestId) { + ...on PullRequest { + reviews(first: 1, author: $author, states: [PENDING]) { nodes { id } } + } + } +} + +query PullRequestComments($owner:String!, $name:String!, $number:Int!, $first:Int=100) { + repository(owner:$owner, name:$name) { + pullRequest(number:$number) { + reviews(first:$first) { + nodes { + comments(first:100) { + nodes { ...Comment } + } + } + } + } + } +} + +mutation AddComment($input: AddPullRequestReviewCommentInput!) { + addPullRequestReviewComment(input: $input) { + comment { + ...Comment + } + } +} + +mutation StartReview($input: AddPullRequestReviewInput!) { + addPullRequestReview(input: $input) { + pullRequestReview { id } + } +} + +mutation SubmitReview($id: ID!) { + submitPullRequestReview(input: { + event: COMMENT, + pullRequestReviewId: $id + }) { + pullRequestReview { + comments(first:100) { + nodes { ...Comment } + } + } + } +} + +mutation DeleteReview($input: DeletePullRequestReviewInput!) { + deletePullRequestReview(input: $input) { + pullRequestReview { + comments(first:100) { + nodes { ...Comment } + } + } + } +} \ No newline at end of file diff --git a/src/test/common/telemetry.test.ts b/src/test/common/telemetry.test.ts index e50c8528cc..86313b3ef6 100644 --- a/src/test/common/telemetry.test.ts +++ b/src/test/common/telemetry.test.ts @@ -16,7 +16,8 @@ const context = { extensionPath: '', asAbsolutePath: relativePath => `/${relativePath}`, storagePath: '', - logPath: '' + logPath: '', + globalStoragePath: '' }; describe('Telemetry', () => { diff --git a/src/test/github/pullRequestModel.test.ts b/src/test/github/pullRequestModel.test.ts index 213a92031e..1b90616649 100644 --- a/src/test/github/pullRequestModel.test.ts +++ b/src/test/github/pullRequestModel.test.ts @@ -143,6 +143,7 @@ const pr = { sha: '', user, }, + node_id: '1' }; describe('PullRequestModel', () => { diff --git a/src/typings/vscode.proposed.d.ts b/src/typings/vscode.proposed.d.ts index 415aa81b9c..f5bc10dcb5 100644 --- a/src/typings/vscode.proposed.d.ts +++ b/src/typings/vscode.proposed.d.ts @@ -3,14 +3,49 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// This is the place for API experiments and proposals. +/** + * This is the place for API experiments and proposals. + * These API are NOT stable and subject to change. They are only available in the Insiders + * distribution and CANNOT be used in published extensions. + * + * To test these API in local environment: + * - Use Insiders release of VS Code. + * - Add `"enableProposedApi": true` to your package.json. + * - Copy this file to your project. + */ declare module 'vscode' { - export namespace window { - export function sampleFunction(): Thenable; + //#region Joh - selection range provider + + export interface SelectionRangeProvider { + /** + * Provide selection ranges starting at a given position. The first range must [contain](#Range.contains) + * position and subsequent ranges must contain the previous range. + * @param document + * @param position + * @param token + */ + provideSelectionRanges(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + } + + export namespace languages { + export function registerSelectionRangeProvider(selector: DocumentSelector, provider: SelectionRangeProvider): Disposable; + } + + //#endregion + + //#region Joh - read/write in chunks + + export interface FileSystemProvider { + open?(resource: Uri): number | Thenable; + close?(fd: number): void | Thenable; + read?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): number | Thenable; + write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): number | Thenable; } + //#endregion + //#region Rob: search provider /** @@ -22,6 +57,11 @@ declare module 'vscode' { */ pattern: string; + /** + * Whether or not `pattern` should match multiple lines of text. + */ + isMultiline?: boolean; + /** * Whether or not `pattern` should be interpreted as a regular expression. */ @@ -75,6 +115,30 @@ declare module 'vscode' { * See the vscode setting `"search.followSymlinks"`. */ followSymlinks: boolean; + + /** + * Whether global files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useGlobalIgnoreFiles"`. + */ + useGlobalIgnoreFiles: boolean; + + } + + /** + * Options to specify the size of the result text preview. + * These options don't affect the size of the match itself, just the amount of preview text. + */ + export interface TextSearchPreviewOptions { + /** + * The maximum number of lines in the preview. + * Only search providers that support multiline search will ever return more than one line in the match. + */ + matchLines: number; + + /** + * The maximum number of characters included per line. + */ + charsPerLine: number; } /** @@ -87,9 +151,9 @@ declare module 'vscode' { maxResults: number; /** - * TODO@roblou - total length? # of context lines? leading and trailing # of chars? + * Options to specify the size of the result text preview. */ - previewOptions?: any; + previewOptions?: TextSearchPreviewOptions; /** * Exclude files larger than `maxFileSize` in bytes. @@ -101,6 +165,30 @@ declare module 'vscode' { * See the vscode setting `"files.encoding"` */ encoding?: string; + + /** + * Number of lines of context to include before each match. + */ + beforeContext?: number; + + /** + * Number of lines of context to include after each match. + */ + afterContext?: number; + } + + /** + * Information collected when text search is complete. + */ + export interface TextSearchComplete { + /** + * Whether the search hit the limit on the maximum number of search results. + * `maxResults` on [`TextSearchOptions`](#TextSearchOptions) specifies the max number of results. + * - If exactly that number of matches exist, this should be false. + * - If `maxResults` matches are returned and more exist, this should be true. + * - If search hits an internal limit which is less than `maxResults`, this should be true. + */ + limitHit?: boolean; } /** @@ -120,7 +208,13 @@ declare module 'vscode' { /** * The maximum number of results to be returned. */ - maxResults: number; + maxResults?: number; + + /** + * A CancellationToken that represents the session for this search query. If the provider chooses to, this object can be used as the key for a cache, + * and searches with the same session object can search the same cache. When the token is cancelled, the session is complete and the cache can be cleared. + */ + session?: CancellationToken; } /** @@ -128,39 +222,65 @@ declare module 'vscode' { */ export interface FileIndexOptions extends SearchOptions { } - export interface TextSearchResultPreview { + /** + * A preview of the text result. + */ + export interface TextSearchMatchPreview { /** - * The matching line of text, or a portion of the matching line that contains the match. - * For now, this can only be a single line. + * The matching lines of text, or a portion of the matching line that contains the match. */ text: string; /** * The Range within `text` corresponding to the text of the match. + * The number of matches must match the TextSearchMatch's range property. */ - match: Range; + matches: Range | Range[]; } /** * A match from a text search */ - export interface TextSearchResult { + export interface TextSearchMatch { /** * The uri for the matching document. */ uri: Uri; /** - * The range of the match within the document. + * The range of the match within the document, or multiple ranges for multiple matches. */ - range: Range; + ranges: Range | Range[]; + + /** + * A preview of the text match. + */ + preview: TextSearchMatchPreview; + } + + /** + * A line of context surrounding a TextSearchMatch. + */ + export interface TextSearchContext { + /** + * The uri for the matching document. + */ + uri: Uri; /** - * A preview of the matching line + * One line of text. + * previewOptions.charsPerLine applies to this */ - preview: TextSearchResultPreview; + text: string; + + /** + * The line number of this line of context. + */ + lineNumber: number; } + export type TextSearchResult = TextSearchMatch | TextSearchContext; + /** * A FileIndexProvider provides a list of files in the given folder. VS Code will filter that list for searching with quickopen or from other extensions. * @@ -178,7 +298,7 @@ declare module 'vscode' { * @param options A set of options to consider while searching. * @param token A cancellation token. */ - provideFileIndex(options: FileIndexOptions, token: CancellationToken): Thenable; + provideFileIndex(options: FileIndexOptions, token: CancellationToken): ProviderResult; } /** @@ -200,7 +320,7 @@ declare module 'vscode' { * @param progress A progress callback that must be invoked for all results. * @param token A cancellation token. */ - provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): Thenable; + provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): ProviderResult; } /** @@ -214,7 +334,7 @@ declare module 'vscode' { * @param progress A progress callback that must be invoked for all results. * @param token A cancellation token. */ - provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Thenable; + provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): ProviderResult; } /** @@ -246,6 +366,12 @@ declare module 'vscode' { */ useIgnoreFiles?: boolean; + /** + * Whether global files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useGlobalIgnoreFiles"`. + */ + useGlobalIgnoreFiles?: boolean; + /** * Whether symlinks should be followed while searching. * See the vscode setting `"search.followSymlinks"`. @@ -257,6 +383,21 @@ declare module 'vscode' { * See the vscode setting `"files.encoding"` */ encoding?: string; + + /** + * Options to specify the size of the result text preview. + */ + previewOptions?: TextSearchPreviewOptions; + + /** + * Number of lines of context to include before each match. + */ + beforeContext?: number; + + /** + * Number of lines of context to include after each match. + */ + afterContext?: number; } export namespace workspace { @@ -305,7 +446,7 @@ declare module 'vscode' { * @param token A token that can be used to signal cancellation to the underlying search engine. * @return A thenable that resolves when the search is complete. */ - export function findTextInFiles(query: TextSearchQuery, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; + export function findTextInFiles(query: TextSearchQuery, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; /** * Search text in files across all [workspace folders](#workspace.workspaceFolders) in the workspace. @@ -315,7 +456,7 @@ declare module 'vscode' { * @param token A token that can be used to signal cancellation to the underlying search engine. * @return A thenable that resolves when the search is complete. */ - export function findTextInFiles(query: TextSearchQuery, options: FindTextInFilesOptions, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; + export function findTextInFiles(query: TextSearchQuery, options: FindTextInFilesOptions, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; } //#endregion @@ -384,37 +525,36 @@ declare module 'vscode' { //#region André: debug - /** - * Represents a debug adapter executable and optional arguments passed to it. - */ - export class DebugAdapterExecutable { - /** - * The command path of the debug adapter executable. - * A command must be either an absolute path or the name of an executable looked up via the PATH environment variable. - * The special value 'node' will be mapped to VS Code's built-in node runtime. - */ - readonly command: string; + // deprecated - /** - * Optional arguments passed to the debug adapter executable. - */ - readonly args: string[]; + export interface DebugAdapterTracker { + // VS Code -> Debug Adapter + startDebugAdapter?(): void; + toDebugAdapter?(message: any): void; + stopDebugAdapter?(): void; - /** - * Create a new debug adapter specification. - */ - constructor(command: string, args?: string[]); + // Debug Adapter -> VS Code + fromDebugAdapter?(message: any): void; + debugAdapterError?(error: Error): void; + debugAdapterExit?(code?: number, signal?: string): void; } export interface DebugConfigurationProvider { /** - * This optional method is called just before a debug adapter is started to determine its executable path and arguments. - * Registering more than one debugAdapterExecutable for a type results in an error. - * @param folder The workspace folder from which the configuration originates from or undefined for a folderless setup. - * @param token A cancellation token. - * @return a [debug adapter's executable and optional arguments](#DebugAdapterExecutable) or undefined. + * Deprecated, use DebugAdapterDescriptorFactory.provideDebugAdapter instead. + * @deprecated Use DebugAdapterDescriptorFactory.createDebugAdapterDescriptor instead */ debugAdapterExecutable?(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult; + + /** + * Deprecated, use DebugAdapterTrackerFactory.createDebugAdapterTracker instead. + * @deprecated Use DebugAdapterTrackerFactory.createDebugAdapterTracker instead + * + * The optional method 'provideDebugAdapterTracker' is called at the start of a debug session to provide a tracker that gives access to the communication between VS Code and a Debug Adapter. + * @param session The [debug session](#DebugSession) for which the tracker will be used. + * @param token A cancellation token. + */ + provideDebugAdapterTracker?(session: DebugSession, workspaceFolder: WorkspaceFolder | undefined, config: DebugConfiguration, token?: CancellationToken): ProviderResult; } //#endregion @@ -437,10 +577,13 @@ declare module 'vscode' { export namespace env { /** * Current logging level. - * - * @readonly */ export const logLevel: LogLevel; + + /** + * An [event](#Event) that fires when the log level has changed. + */ + export const onDidChangeLogLevel: Event; } //#endregion @@ -512,6 +655,21 @@ declare module 'vscode' { //#endregion + //#region Joao: SCM Input Box + + /** + * Represents the input box in the Source Control viewlet. + */ + export interface SourceControlInputBox { + + /** + * Controls whether the input box is visible (default is `true`). + */ + visible: boolean; + } + + //#endregion + //#region Comments /** * Comments provider related APIs are still in early stages, they may be changed significantly during our API experiments. @@ -527,6 +685,11 @@ declare module 'vscode' { * The ranges of the document which support commenting. */ commentingRanges?: Range[]; + + /** + * If it's in draft mode or not + */ + inDraftMode?: boolean; } export enum CommentThreadCollapsibleState { @@ -620,6 +783,8 @@ declare module 'vscode' { * The command to be executed if the comment is selected in the Comments Panel */ command?: Command; + + isDraft: boolean; } export interface CommentThreadChangedEvent { @@ -637,6 +802,11 @@ declare module 'vscode' { * Changed comment threads. */ readonly changed: CommentThread[]; + + /** + * Changed draft mode + */ + readonly inDraftMode?: boolean; } interface DocumentCommentProvider { @@ -656,7 +826,7 @@ declare module 'vscode' { replyToCommentThread(document: TextDocument, range: Range, commentThread: CommentThread, text: string, token: CancellationToken): Promise; /** - * Called when a user edits the comment body to the be new text text. + * Called when a user edits the comment body to the be new text. */ editComment?(document: TextDocument, comment: Comment, text: string, token: CancellationToken): Promise; @@ -665,6 +835,14 @@ declare module 'vscode' { */ deleteComment?(document: TextDocument, comment: Comment, token: CancellationToken): Promise; + startDraft?(document: TextDocument, token: CancellationToken): Promise; + deleteDraft?(document: TextDocument, token: CancellationToken): Promise; + finishDraft?(document: TextDocument, token: CancellationToken): Promise; + + startDraftLabel?: string; + deleteDraftLabel?: string; + finishDraftLabel?: string; + /** * Notify of updates to comment threads. */ @@ -814,19 +992,6 @@ declare module 'vscode' { } export namespace window { - /** - * The currently active terminal or `undefined`. The active terminal is the one that - * currently has focus or most recently had focus. - */ - export const activeTerminal: Terminal | undefined; - - /** - * An [event](#Event) which fires when the [active terminal](#window.activeTerminal) - * has changed. *Note* that the event also fires when the active terminal changes - * to `undefined`. - */ - export const onDidChangeActiveTerminal: Event; - /** * Create a [TerminalRenderer](#TerminalRenderer). * @@ -862,4 +1027,71 @@ declare module 'vscode' { export const onDidRenameFile: Event; } //#endregion + + //#region Alex - OnEnter enhancement + export interface OnEnterRule { + /** + * This rule will only execute if the text above the this line matches this regular expression. + */ + oneLineAboveText?: RegExp; + } + //#endregion + + //#region Tree View + + export interface TreeView { + + /** + * An optional human-readable message that will be rendered in the view. + */ + message?: string | MarkdownString; + + } + + /** + * Label describing the [Tree item](#TreeItem) + */ + export interface TreeItemLabel { + + /** + * A human-readable string describing the [Tree item](#TreeItem). + */ + label: string; + + /** + * Ranges in the label to highlight. A range is defined as a tuple of two number where the + * first is the inclusive start index and the second the exclusive end index + */ + highlights?: [number, number][]; + + } + + export class TreeItem2 extends TreeItem { + /** + * Label describing this item. When `falsy`, it is derived from [resourceUri](#TreeItem.resourceUri). + */ + label?: string | TreeItemLabel | /* for compilation */ any; + + /** + * @param label Label describing this item + * @param collapsibleState [TreeItemCollapsibleState](#TreeItemCollapsibleState) of the tree item. Default is [TreeItemCollapsibleState.None](#TreeItemCollapsibleState.None) + */ + constructor(label: TreeItemLabel, collapsibleState?: TreeItemCollapsibleState); + } + //#endregion + + //#region Extension Context + export interface ExtensionContext { + + /** + * An absolute file path in which the extension can store gloabal state. + * The directory might not exist on disk and creation is + * up to the extension. However, the parent directory is guaranteed to be existent. + * + * Use [`globalState`](#ExtensionContext.globalState) to store key value data. + */ + globalStoragePath: string; + + } + //#endregion } diff --git a/src/view/reviewManager.ts b/src/view/reviewManager.ts index fa8d9aa820..aeb4681ad7 100644 --- a/src/view/reviewManager.ts +++ b/src/view/reviewManager.ts @@ -23,6 +23,7 @@ import { providePRDocumentComments, PRNode } from './treeNodes/pullRequestNode'; import { PullRequestOverviewPanel } from '../github/pullRequestOverview'; import { Remote, parseRepositoryRemotes } from '../common/remote'; import { RemoteQuickPickItem } from './quickpick'; +import { PullRequestModel } from '../github/pullRequestModel'; export class ReviewManager implements vscode.DecorationProvider { public static ID = 'Review'; @@ -339,14 +340,20 @@ export class ReviewManager implements vscode.DecorationProvider { throw new Error('Unable to find matching file'); } - const comment = await this._prManager.createCommentReply(this._prManager.activePullRequest, text, thread.threadId); + const commentFromThread = this._comments.find(c => c.id.toString() === thread.threadId); + if (!commentFromThread) { + throw new Error('Unable to find thread to respond to.'); + } + + const comment = await this._prManager.createCommentReply(this._prManager.activePullRequest, text, commentFromThread); thread.comments.push({ commentId: comment.id.toString(), body: new vscode.MarkdownString(comment.body), userName: comment.user.login, gravatar: comment.user.avatar_url, canEdit: comment.canEdit, - canDelete: comment.canDelete + canDelete: comment.canDelete, + isDraft: comment.isDraft }); matchedFile.comments.push(comment); @@ -389,7 +396,8 @@ export class ReviewManager implements vscode.DecorationProvider { userName: rawComment.user.login, gravatar: rawComment.user.avatar_url, canEdit: rawComment.canEdit, - canDelete: rawComment.canDelete + canDelete: rawComment.canDelete, + isDraft: rawComment.isDraft }; let commentThread: vscode.CommentThread = { @@ -480,6 +488,13 @@ export class ReviewManager implements vscode.DecorationProvider { }] }); } + + this._onDidChangeDocumentCommentThreads.fire({ + added: [], + changed: [], + removed: [], + inDraftMode: await this._prManager.inDraftMode(this._prManager.activePullRequest) + }); } const indexInAllComments = this._comments.findIndex(c => c.id.toString() === comment.commentId); @@ -573,7 +588,8 @@ export class ReviewManager implements vscode.DecorationProvider { this._onDidChangeDocumentCommentThreads.fire({ added: added, removed: removed, - changed: changed + changed: changed, + inDraftMode: await this._prManager.inDraftMode(this._prManager.activePullRequest) }); this._onDidChangeWorkspaceCommentThreads.fire({ @@ -720,7 +736,8 @@ export class ReviewManager implements vscode.DecorationProvider { ] }, canEdit: comment.canEdit, - canDelete: comment.canDelete + canDelete: comment.canDelete, + isDraft: comment.isDraft }; }), collapsibleState: collapsibleState @@ -772,7 +789,8 @@ export class ReviewManager implements vscode.DecorationProvider { gravatar: comment.user.avatar_url, command: command, canEdit: comment.canEdit, - canDelete: comment.canDelete + canDelete: comment.canDelete, + isDraft: comment.isDraft }; }), collapsibleState: collapsibleState @@ -822,6 +840,7 @@ export class ReviewManager implements vscode.DecorationProvider { } private registerCommentProvider() { + const supportsGraphQL = this._prManager.activePullRequest && (this._prManager.activePullRequest as PullRequestModel).githubRepository.supportsGraphQl(); this._documentCommentProvider = vscode.workspace.registerDocumentCommentProvider({ onDidChangeCommentThreads: this._onDidChangeDocumentCommentThreads.event, provideDocumentComments: async (document: vscode.TextDocument, token: vscode.CancellationToken): Promise => { @@ -869,11 +888,13 @@ export class ReviewManager implements vscode.DecorationProvider { return { threads: this.fileCommentsToCommentThreads(matchedFile, matchingComments, vscode.CommentThreadCollapsibleState.Collapsed), commentingRanges: ranges, + inDraftMode: await this._prManager.inDraftMode(this._prManager.activePullRequest) }; } if (document.uri.scheme === 'pr') { - return providePRDocumentComments(document, this._prNumber, this._localFileChanges); + const inDraftMode = await this._prManager.inDraftMode(this._prManager.activePullRequest); + return providePRDocumentComments(document, this._prNumber, this._localFileChanges, inDraftMode); } if (document.uri.scheme === 'review') { @@ -960,14 +981,16 @@ export class ReviewManager implements vscode.DecorationProvider { userName: comment.user.login, gravatar: comment.user.avatar_url, canEdit: comment.canEdit, - canDelete: comment.canDelete + canDelete: comment.canDelete, + isDraft: comment.isDraft }; }), collapsibleState: vscode.CommentThreadCollapsibleState.Expanded }); return { - threads: ret + threads: ret, + inDraftMode: await this._prManager.inDraftMode(this._prManager.activePullRequest) }; } } @@ -975,7 +998,13 @@ export class ReviewManager implements vscode.DecorationProvider { createNewCommentThread: this.createNewCommentThread.bind(this), replyToCommentThread: this.replyToCommentThread.bind(this), editComment: this.editComment.bind(this), - deleteComment: this.deleteComment.bind(this) + deleteComment: this.deleteComment.bind(this), + startDraft: supportsGraphQL ? this.startDraft.bind(this) : undefined, + deleteDraft: supportsGraphQL ? this.deleteDraft.bind(this) : undefined, + finishDraft: supportsGraphQL ? this.finishDraft.bind(this) : undefined, + startDraftLabel: 'Start Review', + deleteDraftLabel: 'Delete Review', + finishDraftLabel: 'Submit Review' }); this._workspaceCommentProvider = vscode.workspace.registerWorkspaceCommentProvider({ @@ -992,6 +1021,92 @@ export class ReviewManager implements vscode.DecorationProvider { }); } + private async startDraft(_document: vscode.TextDocument, _token: vscode.CancellationToken): Promise { + await this._prManager.startReview(this._prManager.activePullRequest); + this._onDidChangeDocumentCommentThreads.fire({ + added: [], + changed: [], + removed: [], + inDraftMode: true + }); + } + + private async deleteDraft(_document: vscode.TextDocument, _token: vscode.CancellationToken) { + const deletedReviewComments = await this._prManager.deleteReview(this._prManager.activePullRequest); + + const removed = []; + const changed = []; + + const oldCommentThreads = this.allCommentsToCommentThreads(this._comments, vscode.CommentThreadCollapsibleState.Expanded); + oldCommentThreads.forEach(thread => { + thread.comments = thread.comments.filter(comment => !deletedReviewComments.some(deletedComment => deletedComment.id.toString() === comment.commentId)); + if (!thread.comments.length) { + removed.push(thread); + } else { + changed.push(thread); + } + }); + + const commentsByFile = groupBy(deletedReviewComments, comment => comment.path); + for (let filePath in commentsByFile) { + const matchedFile = this._localFileChanges.find(fileChange => fileChange.fileName === filePath); + if (matchedFile) { + const deletedFileComments = commentsByFile[filePath]; + matchedFile.comments = matchedFile.comments.filter(comment => !deletedFileComments.some(deletedComment => deletedComment.id === comment.id)); + } + } + + this._comments = this._comments.filter(comment => !deletedReviewComments.some(deletedComment => deletedComment.id === comment.id)); + + this._onDidChangeDocumentCommentThreads.fire({ + added: [], + changed, + removed, + inDraftMode: false + }); + + this._onDidChangeWorkspaceCommentThreads.fire({ + added: [], + changed, + removed, + inDraftMode: false + }); + } + + private async finishDraft(_document: vscode.TextDocument, _token: vscode.CancellationToken) { + try { + const comments = await this._prManager.submitReview(this._prManager.activePullRequest); + + this._comments.forEach(comment => { + if (comments.some(updatedComment => updatedComment.id === comment.id)) { + comment.isDraft = false; + } + }); + + const commentsByFile = groupBy(comments, comment => comment.path); + for (let filePath in commentsByFile) { + const matchedFile = this._localFileChanges.find(fileChange => fileChange.fileName === filePath); + if (matchedFile) { + const fileComments = commentsByFile[filePath]; + matchedFile.comments.forEach(comment => { + if (fileComments.some(updatedComment => updatedComment.id === comment.id)) { + comment.isDraft = false; + } + }); + } + } + + this._onDidChangeDocumentCommentThreads.fire({ + added: [], + changed: this.allCommentsToCommentThreads(comments, vscode.CommentThreadCollapsibleState.Expanded), + removed: [], + inDraftMode: false + }); + } catch (e) { + vscode.window.showErrorMessage(`Failed to submit the review: ${e}`); + } + } + private findMatchedFileChange(fileChanges: (GitFileChangeNode | RemoteFileChangeNode)[], uri: vscode.Uri): GitFileChangeNode { let query = fromReviewUri(uri); let matchedFiles = fileChanges.filter(fileChange => { diff --git a/src/view/treeNodes/pullRequestNode.ts b/src/view/treeNodes/pullRequestNode.ts index c37faaf349..a67eb2a9da 100644 --- a/src/view/treeNodes/pullRequestNode.ts +++ b/src/view/treeNodes/pullRequestNode.ts @@ -23,7 +23,8 @@ import { getPRDocumentCommentProvider } from '../prDocumentCommentProvider'; export function providePRDocumentComments( document: vscode.TextDocument, prNumber: number, - fileChanges: (RemoteFileChangeNode | InMemFileChangeNode)[]) { + fileChanges: (RemoteFileChangeNode | InMemFileChangeNode)[], + inDraftMode: boolean) { const params = fromPRUri(document.uri); if (params.prNumber !== prNumber) { @@ -69,6 +70,7 @@ export function providePRDocumentComments( return { threads: [], commentingRanges, + inDraftMode }; } @@ -101,7 +103,8 @@ export function providePRDocumentComments( userName: comment.user.login, gravatar: comment.user.avatar_url, canEdit: comment.canEdit, - canDelete: comment.canDelete + canDelete: comment.canDelete, + isDraft: comment.isDraft }; }), collapsibleState: vscode.CommentThreadCollapsibleState.Expanded, @@ -111,6 +114,7 @@ export function providePRDocumentComments( return { threads, commentingRanges, + inDraftMode }; } @@ -144,7 +148,8 @@ function commentsToCommentThreads(fileChange: InMemFileChangeNode, comments: Com userName: comment.user.login, gravatar: comment.user.avatar_url, canEdit: comment.canEdit, - canDelete: comment.canDelete + canDelete: comment.canDelete, + isDraft: comment.isDraft }; }), collapsibleState: vscode.CommentThreadCollapsibleState.Expanded, @@ -373,7 +378,8 @@ export class PRNode extends TreeNode { this._onDidChangeCommentThreads.fire({ added: added, removed: removed, - changed: changed + changed: changed, + inDraftMode: await this._prManager.inDraftMode(this.pullRequestModel) }); // this._onDidChangeDecorations.fire(); } @@ -484,7 +490,8 @@ export class PRNode extends TreeNode { userName: rawComment.user.login, gravatar: rawComment.user.avatar_url, canEdit: rawComment.canEdit, - canDelete: rawComment.canDelete + canDelete: rawComment.canDelete, + isDraft: rawComment.isDraft }; fileChange.comments.push(rawComment); @@ -520,20 +527,34 @@ export class PRNode extends TreeNode { if (index > -1) { fileChange.comments.splice(index, 1); } + + const inDraftMode = await this._prManager.inDraftMode(this.pullRequestModel); + this._onDidChangeCommentThreads.fire({ + added: [], + changed: [], + removed: [], + inDraftMode + }); } private async replyToCommentThread(document: vscode.TextDocument, _range: vscode.Range, thread: vscode.CommentThread, text: string) { try { const fileChange = this.findMatchingFileNode(document.uri); - const rawComment = await this._prManager.createCommentReply(this.pullRequestModel, text, thread.threadId); + const commentFromThread = fileChange.comments.find(c => c.id.toString() === thread.threadId); + if (!commentFromThread) { + throw new Error('Unable to find thread to respond to.'); + } + + const rawComment = await this._prManager.createCommentReply(this.pullRequestModel, text, commentFromThread); thread.comments.push({ commentId: rawComment.id.toString(), body: new vscode.MarkdownString(rawComment.body), userName: rawComment.user.login, gravatar: rawComment.user.avatar_url, canEdit: rawComment.canEdit, - canDelete: rawComment.canDelete + canDelete: rawComment.canDelete, + isDraft: rawComment.isDraft }); fileChange.comments.push(rawComment); @@ -546,7 +567,8 @@ export class PRNode extends TreeNode { private async provideDocumentComments(document: vscode.TextDocument, _token: vscode.CancellationToken): Promise { if (document.uri.scheme === 'pr') { - return providePRDocumentComments(document, this.pullRequestModel.prNumber, this._fileChanges); + const inDraftMode = await this._prManager.inDraftMode(this.pullRequestModel); + return providePRDocumentComments(document, this.pullRequestModel.prNumber, this._fileChanges, inDraftMode); } return null;