diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index a702d0280d7..71ad6ba5f97 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1009,6 +1009,7 @@ export interface Comment { readonly userName: string; readonly gravatar: string; readonly canEdit?: boolean; + readonly canDelete?: boolean; readonly command?: Command; } @@ -1041,6 +1042,7 @@ export interface DocumentCommentProvider { createNewCommentThread(resource: URI, range: Range, text: string, token: CancellationToken): Promise; replyToCommentThread(resource: URI, range: Range, thread: CommentThread, text: string, token: CancellationToken): Promise; editComment(resource: URI, comment: Comment, text: string, token: CancellationToken): Promise; + deleteComment(resource: URI, comment: Comment, token: CancellationToken): Promise; onDidChangeCommentThreads(): Event; } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 297b38e2347..12a88778046 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -698,6 +698,14 @@ declare module 'vscode' { */ canEdit?: boolean; + /** + * Whether the current user has permission to delete the comment. + * + * This will be treated as false if the comment is provided by a `WorkspaceCommentProvider`, or + * if it is provided by a `DocumentCommentProvider` and no `deleteComment` method is given. + */ + canDelete?: boolean; + /** * The command to be executed if the comment is selected in the Comments Panel */ @@ -742,6 +750,11 @@ declare module 'vscode' { */ editComment?(document: TextDocument, comment: Comment, text: string, token: CancellationToken): Promise; + /** + * Called when a user deletes the comment. + */ + deleteComment?(document: TextDocument, comment: Comment, token: CancellationToken): Promise; + /** * Notify of updates to comment threads. */ diff --git a/src/vs/workbench/api/electron-browser/mainThreadComments.ts b/src/vs/workbench/api/electron-browser/mainThreadComments.ts index 07e70cbc2ed..52e2fff1ac7 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadComments.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadComments.ts @@ -80,6 +80,9 @@ export class MainThreadComments extends Disposable implements MainThreadComments }, editComment: async (uri, comment, text, token) => { return this._proxy.$editComment(handle, uri, comment, text); + }, + deleteComment: async (uri, comment, token) => { + return this._proxy.$deleteComment(handle, uri, comment); } } ); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index a740addb675..200aa028b52 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -997,6 +997,7 @@ export interface ExtHostCommentsShape { $createNewCommentThread(handle: number, document: UriComponents, range: IRange, text: string): Thenable; $replyToCommentThread(handle: number, document: UriComponents, range: IRange, commentThread: modes.CommentThread, text: string): Thenable; $editComment(handle: number, document: UriComponents, comment: modes.Comment, text: string): Thenable; + $deleteComment(handle: number, document: UriComponents, comment: modes.Comment): Thenable; $provideWorkspaceComments(handle: number): Thenable; } diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts index 772e0878504..80179074066 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -106,6 +106,19 @@ export class ExtHostComments implements ExtHostCommentsShape { }).then(comment => convertToComment(provider, comment, this._commandsConverter)); } + $deleteComment(handle: number, uri: UriComponents, comment: modes.Comment): Thenable { + const data = this._documents.getDocumentData(URI.revive(uri)); + + if (!data || !data.document) { + throw new Error('Unable to retrieve document from URI'); + } + + const provider = this._documentProviders.get(handle); + return asThenable(() => { + return provider.deleteComment(data.document, convertFromComment(comment), CancellationToken.None); + }); + } + $provideDocumentComments(handle: number, uri: UriComponents): Thenable { const data = this._documents.getDocumentData(URI.revive(uri)); if (!data || !data.document) { @@ -178,18 +191,21 @@ function convertFromComment(comment: modes.Comment): vscode.Comment { body: extHostTypeConverter.MarkdownString.to(comment.body), userName: comment.userName, gravatar: comment.gravatar, - canEdit: comment.canEdit + canEdit: comment.canEdit, + canDelete: comment.canDelete }; } function convertToComment(provider: vscode.DocumentCommentProvider | vscode.WorkspaceCommentProvider, vscodeComment: vscode.Comment, commandsConverter: CommandsConverter): modes.Comment { const canEdit = !!(provider as vscode.DocumentCommentProvider).editComment && vscodeComment.canEdit; + const canDelete = !!(provider as vscode.DocumentCommentProvider).deleteComment && vscodeComment.canDelete; return { commentId: vscodeComment.commentId, body: extHostTypeConverter.MarkdownString.from(vscodeComment.body), userName: vscodeComment.userName, gravatar: vscodeComment.gravatar, canEdit: canEdit, + canDelete: canDelete, command: vscodeComment.command ? commandsConverter.toInternal(vscodeComment.command) : null }; } diff --git a/src/vs/workbench/parts/comments/electron-browser/commentNode.ts b/src/vs/workbench/parts/comments/electron-browser/commentNode.ts index ce207ac1ed6..ff05c4b26db 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentNode.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentNode.ts @@ -26,6 +26,8 @@ import { SimpleCommentEditor } from 'vs/workbench/parts/comments/electron-browse import { KeyCode } from 'vs/base/common/keyCodes'; import { isMacintosh } from 'vs/base/common/platform'; import { Selection } from 'vs/editor/common/core/selection'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { Emitter, Event } from 'vs/base/common/event'; const UPDATE_COMMENT_LABEL = nls.localize('label.updateComment', "Update comment"); const UPDATE_IN_PROGRESS_LABEL = nls.localize('label.updatingComment', "Updating comment..."); @@ -43,6 +45,9 @@ export class CommentNode extends Disposable { private _updateCommentButton: Button; private _errorEditingContainer: HTMLElement; + private _deleteAction: Action; + private _onDidDelete = new Emitter(); + public get domNode(): HTMLElement { return this._domNode; } @@ -56,7 +61,8 @@ export class CommentNode extends Disposable { private instantiationService: IInstantiationService, private commentService: ICommentService, private modelService: IModelService, - private modeService: IModeService + private modeService: IModeService, + private dialogService: IDialogService ) { super(); @@ -78,6 +84,10 @@ export class CommentNode extends Disposable { this._clearTimeout = null; } + public get onDidDelete(): Event { + return this._onDidDelete.event; + } + private createHeader(commentDetailsContainer: HTMLElement): void { const header = dom.append(commentDetailsContainer, dom.$('div.comment-title')); const author = dom.append(header, dom.$('strong.author')); @@ -89,6 +99,11 @@ export class CommentNode extends Disposable { actions.push(this._editAction); } + if (this.comment.canDelete) { + this._deleteAction = this.createDeleteAction(); + actions.push(this._deleteAction); + } + if (actions.length) { const actionsContainer = dom.append(header, dom.$('.comment-actions.hidden')); const actionBar = new ActionBar(actionsContainer, {}); @@ -158,6 +173,25 @@ export class CommentNode extends Disposable { } } + private createDeleteAction(): Action { + return new Action('comment.delete', nls.localize('label.delete', "Delete"), 'octicon octicon-x', true, () => { + return this.dialogService.confirm({ + title: nls.localize('deleteCommentTitle', "Delete Comment"), + message: nls.localize('confirmDelete', "Delete comment?"), + type: 'question', + primaryButton: nls.localize('label.delete', "Delete") + }).then(result => { + if (result.confirmed) { + this.commentService.deleteComment(this.owner, this.resource, this.comment).then((didDelete) => { + if (didDelete) { + this._onDidDelete.fire(this); + } + }); + } + }); + }); + } + private createEditAction(commentDetailsContainer: HTMLElement): Action { return new Action('comment.edit', nls.localize('label.edit', "Edit"), 'octicon octicon-pencil', true, () => { this._body.classList.add('hidden'); diff --git a/src/vs/workbench/parts/comments/electron-browser/commentService.ts b/src/vs/workbench/parts/comments/electron-browser/commentService.ts index 119a1fb5c88..ed8eaf0225c 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentService.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentService.ts @@ -5,6 +5,7 @@ 'use strict'; +import * as nls from 'vs/nls'; import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, Comment } from 'vs/editor/common/modes'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event, Emitter } from 'vs/base/common/event'; @@ -13,6 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { keys } from 'vs/base/common/map'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export const ICommentService = createDecorator('commentService'); @@ -42,6 +44,7 @@ export interface ICommentService { createNewCommentThread(owner: number, resource: URI, range: Range, text: string): Promise; replyToCommentThread(owner: number, resource: URI, range: Range, thread: CommentThread, text: string): Promise; editComment(owner: number, resource: URI, comment: Comment, text: string): Promise; + deleteComment(owner: number, resource: URI, comment: Comment): Promise; getComments(resource: URI): Promise; } @@ -65,7 +68,7 @@ export class CommentService extends Disposable implements ICommentService { private _commentProviders = new Map(); - constructor() { + constructor(@INotificationService private notificationService: INotificationService) { super(); } @@ -125,6 +128,21 @@ export class CommentService extends Disposable implements ICommentService { return null; } + deleteComment(owner: number, resource: URI, comment: Comment): Promise { + const commentProvider = this._commentProviders.get(owner); + + if (commentProvider) { + try { + return commentProvider.deleteComment(resource, comment, CancellationToken.None).then(() => true); + } catch (e) { + this.notificationService.error(nls.localize('commentDeletionError', "Deleting the comment failed: {0}.", e.message)); + return Promise.resolve(false); + } + } + + return Promise.resolve(false); + } + getComments(resource: URI): Promise { const result = []; for (const handle of keys(this._commentProviders)) { diff --git a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts index cc5017cc34c..fd855693a38 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts @@ -38,6 +38,7 @@ import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { CommentNode } from 'vs/workbench/parts/comments/electron-browser/commentNode'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-x'; @@ -83,6 +84,7 @@ export class ReviewZoneWidget extends ZoneWidget { private themeService: IThemeService, private commentService: ICommentService, private openerService: IOpenerService, + private dialogService: IDialogService, editor: ICodeEditor, owner: number, commentThread: modes.CommentThread, @@ -212,17 +214,7 @@ export class ReviewZoneWidget extends ZoneWidget { lastCommentElement = oldCommentNode[0].domNode; newCommentNodeList.unshift(oldCommentNode[0]); } else { - let newElement = new CommentNode( - currentComment, - this.owner, - this.editor.getModel().uri, - this._markdownRenderer, - this.themeService, - this.instantiationService, - this.commentService, - this.modelService, - this.modeService); - this._disposables.push(newElement); + const newElement = this.createNewCommentNode(currentComment); newCommentNodeList.unshift(newElement); if (lastCommentElement) { @@ -260,16 +252,8 @@ export class ReviewZoneWidget extends ZoneWidget { this._commentElements = []; for (let i = 0; i < this._commentThread.comments.length; i++) { - let newCommentNode = new CommentNode(this._commentThread.comments[i], - this.owner, - this.editor.getModel().uri, - this._markdownRenderer, - this.themeService, - this.instantiationService, - this.commentService, - this.modelService, - this.modeService); - this._disposables.push(newCommentNode); + const newCommentNode = this.createNewCommentNode(this._commentThread.comments[i]); + this._commentElements.push(newCommentNode); this._commentsElement.appendChild(newCommentNode.domNode); } @@ -357,6 +341,43 @@ export class ReviewZoneWidget extends ZoneWidget { } } + private createNewCommentNode(comment: modes.Comment): CommentNode { + let newCommentNode = new CommentNode( + comment, + this.owner, + this.editor.getModel().uri, + this._markdownRenderer, + this.themeService, + this.instantiationService, + this.commentService, + this.modelService, + this.modeService, + this.dialogService); + + this._disposables.push(newCommentNode); + this._disposables.push(newCommentNode.onDidDelete(deletedNode => { + const deletedNodeId = deletedNode.comment.commentId; + const deletedElementIndex = arrays.firstIndex(this._commentElements, commentNode => commentNode.comment.commentId === deletedNodeId); + if (deletedElementIndex > -1) { + this._commentElements.splice(deletedElementIndex, 1); + } + + const deletedCommentIndex = arrays.firstIndex(this._commentThread.comments, comment => comment.commentId === deletedNodeId); + if (deletedCommentIndex > -1) { + this._commentThread.comments.splice(deletedCommentIndex, 1); + } + + this._commentsElement.removeChild(deletedNode.domNode); + deletedNode.dispose(); + + if (this._commentThread.comments.length === 0) { + this.dispose(); + } + })); + + return newCommentNode; + } + private async createComment(lineNumber: number): Promise { try { let newCommentThread; diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts index b1b4b18fff5..4fa20e96fd5 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts @@ -34,6 +34,7 @@ import { IModelDecorationOptions } from 'vs/editor/common/model'; import { Color, RGBA } from 'vs/base/common/color'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; export const ctxReviewPanelVisible = new RawContextKey('reviewPanelVisible', false); @@ -184,7 +185,8 @@ export class ReviewController implements IEditorContribution { @IModeService private modeService: IModeService, @IModelService private modelService: IModelService, @ICodeEditorService private codeEditorService: ICodeEditorService, - @IOpenerService private openerService: IOpenerService + @IOpenerService private openerService: IOpenerService, + @IDialogService private dialogService: IDialogService ) { this.editor = editor; this.globalToDispose = []; @@ -356,7 +358,7 @@ export class ReviewController implements IEditorContribution { } }); added.forEach(thread => { - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.editor, e.owner, thread, {}); + let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.editor, e.owner, thread, {}); zoneWidget.display(thread.range.startLineNumber, this._commentingRangeDecorator.commentsOptions); this._commentWidgets.push(zoneWidget); this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); @@ -378,7 +380,7 @@ export class ReviewController implements IEditorContribution { // add new comment this._reviewPanelVisible.set(true); const { replyCommand, ownerId } = newCommentInfo; - this._newCommentWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.editor, ownerId, { + this._newCommentWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.editor, ownerId, { threadId: null, resource: null, comments: [], @@ -505,7 +507,7 @@ export class ReviewController implements IEditorContribution { this._commentInfos.forEach(info => { info.threads.forEach(thread => { - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.editor, info.owner, thread, {}); + let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.editor, info.owner, thread, {}); zoneWidget.display(thread.range.startLineNumber, this._commentingRangeDecorator.commentsOptions); this._commentWidgets.push(zoneWidget); }); diff --git a/src/vs/workbench/parts/comments/electron-browser/media/review.css b/src/vs/workbench/parts/comments/electron-browser/media/review.css index ee23244f647..f4752f7365a 100644 --- a/src/vs/workbench/parts/comments/electron-browser/media/review.css +++ b/src/vs/workbench/parts/comments/electron-browser/media/review.css @@ -44,7 +44,7 @@ } .monaco-editor .review-widget .body .review-comment .comment-actions .action-item { - width: 30px; + width: 22px; } .monaco-editor .review-widget .body .review-comment .comment-title {