diff --git a/extensions/git-extended/src/prView/prProvider.ts b/extensions/git-extended/src/prView/prProvider.ts index a6afd0edaa6..5b3aa7f25a2 100644 --- a/extensions/git-extended/src/prView/prProvider.ts +++ b/extensions/git-extended/src/prView/prProvider.ts @@ -220,6 +220,7 @@ export class PRProvider implements vscode.TreeDataProvider { return { + commentId: comment.id, body: new vscode.MarkdownString(comment.body), userName: comment.user.login, gravatar: comment.user.avatar_url diff --git a/extensions/git-extended/src/review/reviewMode.ts b/extensions/git-extended/src/review/reviewMode.ts index cf043dee64d..d70cdf2b600 100644 --- a/extensions/git-extended/src/review/reviewMode.ts +++ b/extensions/git-extended/src/review/reviewMode.ts @@ -158,6 +158,7 @@ export class ReviewMode { range, comments: comments.map(comment => { return { + commentId: comment.id, body: new vscode.MarkdownString(comment.body), userName: comment.user.login, gravatar: comment.user.avatar_url diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index b4de6251e5e..99c29e142a9 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -952,6 +952,7 @@ export interface NewCommentAction { } export interface Comment { + readonly commentId: string; readonly body: IMarkdownString; readonly userName: string; readonly gravatar: string; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 1a3b887809a..c23f55e9919 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5042,6 +5042,7 @@ declare namespace monaco.languages { } export interface Comment { + readonly commentId: string; readonly body: IMarkdownString; readonly userName: string; readonly gravatar: string; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 4205f9a5043..96368b20c13 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -784,6 +784,7 @@ declare module 'vscode' { } interface Comment { + commentId: string; body: MarkdownString; userName: string; gravatar: string; diff --git a/src/vs/workbench/api/electron-browser/mainThreadComments.ts b/src/vs/workbench/api/electron-browser/mainThreadComments.ts index 034344210fc..992a2aa33f3 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadComments.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadComments.ts @@ -84,6 +84,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments $onDidCommentThreadsChange(handle: number, event: modes.CommentThreadChangedEvent) { // notify comment service + this._commentService.updateComments(event); } $unregisterCommentProvider(handle: number): void { diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts index 877cdabf96d..5dd064c7c65 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -124,6 +124,7 @@ function convertCommentThread(vscodeCommentThread: vscode.CommentThread, command function convertComment(vscodeComment: vscode.Comment): modes.Comment { return { + commentId: vscodeComment.commentId, body: extHostTypeConverter.MarkdownString.from(vscodeComment.body), userName: vscodeComment.userName, gravatar: vscodeComment.gravatar diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts index 2978d1707c6..acf1ea7db11 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts @@ -72,8 +72,8 @@ export class CommentNode { public get domNode(): HTMLElement { return this._domNode; } - constructor(public readonly comment: modes.Comment, public readonly container: HTMLElement) { - this._domNode = $('div.review-comment').appendTo(container).getHTMLElement(); + constructor(public readonly comment: modes.Comment, ) { + this._domNode = $('div.review-comment').getHTMLElement(); let avatar = $('span.float-left').appendTo(this._domNode).getHTMLElement(); let img = $('img.avatar').appendTo(avatar).getHTMLElement(); img.src = comment.gravatar; @@ -96,7 +96,7 @@ export class ReviewZoneWidget extends ZoneWidget { protected _actionbarWidget: ActionBar; private _bodyElement: HTMLElement; private _commentsElement: HTMLElement; - private _commentElements: HTMLElement[]; + private _commentElements: CommentNode[]; private _resizeObserver: any; private _onDidClose = new Emitter(); private _isCollapsed = true; @@ -181,6 +181,61 @@ export class ReviewZoneWidget extends ZoneWidget { this._toggleAction.run(); } + update(commentThread: modes.CommentThread) { + const oldCommentsLen = this._commentElements.length; + const newCommentsLen = commentThread.comments.length; + + let commentElementsToDel: CommentNode[] = []; + let commentElementsToDelIndex: number[] = []; + for (let i = 0; i < oldCommentsLen; i++) { + let comment = this._commentElements[i].comment; + if (!commentThread.comments.some(c => c.commentId === comment.commentId)) { + commentElementsToDelIndex.push(i); + commentElementsToDel.push(this._commentElements[i]); + } + } + + // del removed elements + for (let i = commentElementsToDel.length - 1; i >= 0; i--) { + this._commentElements.splice(commentElementsToDelIndex[i]); + this._commentsElement.removeChild(commentElementsToDel[i].domNode); + } + + if (this._commentElements.length === 0) { + this._commentThread = commentThread; + commentThread.comments.forEach(comment => { + let newElement = new CommentNode(comment); + this._commentElements.push(newElement); + this._commentsElement.appendChild(newElement.domNode); + }); + return; + } + + let lastCommentElement: HTMLElement = null; + let newCommentNodeList: CommentNode[] = []; + for (let i = newCommentsLen - 1; i >= 0; i--) { + let currentComment = commentThread.comments[i]; + let oldCommentNode = this._commentElements.filter(commentNode => commentNode.comment.commentId === currentComment.commentId); + if (oldCommentNode.length) { + lastCommentElement = oldCommentNode[0].domNode; + newCommentNodeList.unshift(oldCommentNode[0]); + } else { + let newElement = new CommentNode(currentComment); + newCommentNodeList.unshift(newElement); + if (lastCommentElement) { + this._commentsElement.insertBefore(newElement.domNode, lastCommentElement); + lastCommentElement = newElement.domNode; + } else { + this._commentsElement.appendChild(newElement.domNode); + lastCommentElement = newElement.domNode; + } + } + } + + this._commentThread = commentThread; + this._commentElements = newCommentNodeList; + } + display(lineNumber: number) { this.show({ lineNumber: lineNumber, column: 1 }, 2); @@ -192,7 +247,9 @@ export class ReviewZoneWidget extends ZoneWidget { this._commentsElement = $('div.comments-container').appendTo(this._bodyElement).getHTMLElement(); this._commentElements = []; for (let i = 0; i < this._commentThread.comments.length; i++) { - this._commentElements.push((new CommentNode(this._commentThread.comments[i], this._commentsElement)).domNode); + let newCommentNode = new CommentNode(this._commentThread.comments[i]); + this._commentElements.push(newCommentNode); + this._commentsElement.appendChild(newCommentNode.domNode); } const commentForm = $('.comment-form').appendTo(this._bodyElement).getHTMLElement(); @@ -206,7 +263,9 @@ export class ReviewZoneWidget extends ZoneWidget { if (newComment) { textArea.value = ''; this._commentThread.comments.push(newComment); - this._commentElements.push((new CommentNode(newComment, this._commentsElement)).domNode); + let newCommentNode = new CommentNode(this._commentThread.comments[i]); + this._commentElements.push(newCommentNode); + this._commentsElement.appendChild(newCommentNode.domNode); let secondaryHeading = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', '); $(this._secondaryHeading).safeInnerHtml(secondaryHeading); } @@ -314,12 +373,45 @@ export class ReviewController implements IEditorContribution { } }); - this.commentService.onDidSetResourceCommentThreads(e => { + this.globalToDispose.push(this.commentService.onDidSetResourceCommentThreads(e => { const editorURI = this.editor && this.editor.getModel() && this.editor.getModel().uri; if (editorURI && editorURI.toString() === e.resource.toString()) { this.setComments(e.commentThreads); } - }); + })); + + this.globalToDispose.push(this.commentService.onDidUpdateCommentThreads(e => { + const editorURI = this.editor && this.editor.getModel() && this.editor.getModel().uri; + if (!editorURI) { + return; + } + let added = e.added.filter(thread => thread.resource.toString() === editorURI.toString()); + let removed = e.removed.filter(thread => thread.resource.toString() === editorURI.toString()); + let changed = e.changed.filter(thread => thread.resource.toString() === editorURI.toString()); + + removed.forEach(thread => { + let matchedZones = this._zoneWidgets.filter(zoneWidget => zoneWidget.commentThread.threadId === thread.threadId); + if (matchedZones.length) { + let matchedZone = matchedZones[0]; + let index = this._zoneWidgets.indexOf(matchedZone); + this._zoneWidgets.splice(index, 1); + } + }); + + changed.forEach(thread => { + let matchedZones = this._zoneWidgets.filter(zoneWidget => zoneWidget.commentThread.threadId === thread.threadId); + if (matchedZones.length) { + let matchedZone = matchedZones[0]; + matchedZone.update(thread); + } + }); + added.forEach(thread => { + let zoneWidget = new ReviewZoneWidget(this.editor, thread, {}, this.themeService, this.commandService); + zoneWidget.display(thread.range.startLineNumber); + this._zoneWidgets.push(zoneWidget); + this._commentThreads.push(thread); + }); + })); this.globalToDispose.push(this.editor.onDidChangeModel(() => this.onModelChanged())); } diff --git a/src/vs/workbench/services/comments/electron-browser/commentService.ts b/src/vs/workbench/services/comments/electron-browser/commentService.ts index 99105c41b40..10faf329903 100644 --- a/src/vs/workbench/services/comments/electron-browser/commentService.ts +++ b/src/vs/workbench/services/comments/electron-browser/commentService.ts @@ -24,10 +24,12 @@ export interface ICommentService { _serviceBrand: any; readonly onDidSetResourceCommentThreads: Event; readonly onDidSetAllCommentThreads: Event; + readonly onDidUpdateCommentThreads: Event; setComments(resource: URI, commentThreads: CommentThread[]): void; setAllComments(commentsByResource: CommentThread[]): void; removeAllComments(): void; registerDataProvider(commentProvider: CommentProvider): void; + updateComments(event: CommentThreadChangedEvent): void; } export class CommentService extends Disposable implements ICommentService { @@ -39,6 +41,9 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidSetAllCommentThreads: Emitter = this._register(new Emitter()); readonly onDidSetAllCommentThreads: Event = this._onDidSetAllCommentThreads.event; + private readonly _onDidUpdateCommentThreads: Emitter = this._register(new Emitter()); + readonly onDidUpdateCommentThreads: Event = this._onDidUpdateCommentThreads.event; + private _commentProvider: CommentProvider; constructor() { @@ -62,6 +67,10 @@ export class CommentService extends Disposable implements ICommentService { this._commentProvider = commentProvider; } + updateComments(event: CommentThreadChangedEvent): void { + this._onDidUpdateCommentThreads.fire(event); + } + async provideComments(model: ITextModel, token: CancellationToken): Promise { return this._commentProvider.provideComments(model, token); }