diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 8d4cb25444e..5d76af1b1fb 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1259,7 +1259,6 @@ export interface CommentThread2 { * @internal */ export interface CommentThread { - commentThreadHandle?: number; // use optional type for now to avoid breaking existing api extensionId: string; threadId: string; resource: string; @@ -1299,6 +1298,8 @@ export interface Comment { readonly canEdit?: boolean; readonly canDelete?: boolean; readonly command?: Command; + readonly editCommand?: Command; + readonly deleteCommand?: Command; readonly isDraft?: boolean; readonly commentReactions?: CommentReaction[]; } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index ba50fdd0169..1b3eb561ded 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -846,6 +846,9 @@ declare module 'vscode' { */ command?: Command; + editCommand?: Command; + deleteCommand?: Command; + isDraft?: boolean; commentReactions?: CommentReaction[]; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadComments.ts b/src/vs/workbench/api/electron-browser/mainThreadComments.ts index 574d274f40e..fcbc11dc440 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadComments.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadComments.ts @@ -218,7 +218,7 @@ export class MainThreadCommentControl { if (this._threads.get(thread).resource === resource.toString()) { ret.push(this._threads.get(thread)); } -} + } return { owner: String(this.handle), @@ -314,8 +314,8 @@ export class MainThreadComments extends Disposable implements MainThreadComments let provider = this._commentControls.get(handle); if (!provider) { - return; - } + return; + } provider.updateComments(commentThreadHandle, comments); } @@ -325,7 +325,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments if (!provider) { return; - } + } provider.updateInput(commentThreadHandle, input); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index acee0716e91..343cc7877b2 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -1097,7 +1097,7 @@ export interface ExtHostProgressShape { export interface ExtHostCommentsShape { $provideDocumentComments(handle: number, document: UriComponents): Promise; $createNewCommentThread(handle: number, document: UriComponents, range: IRange, text: string): Promise; - $onActiveCommentWidgetChange(commentControlhandle: number, commentThread: modes.CommentThread, comment: modes.Comment | undefined, input: string): Promise; + $onActiveCommentWidgetChange(commentControlhandle: number, commentThread: modes.CommentThread2, comment: modes.Comment | undefined, input: string): Promise; $onCommentWidgetInputChange(commentControlhandle: number, value: string): Promise; $replyToCommentThread(handle: number, document: UriComponents, range: IRange, commentThread: modes.CommentThread, text: string): Promise; $editComment(handle: number, document: UriComponents, comment: modes.Comment, text: string): Promise; diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts index 104ed3a0ec0..a6c4d595f84 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -51,7 +51,7 @@ export class ExtHostComments implements ExtHostCommentsShape { if (!commentControl) { return arg; - } + } return commentControl; } else if (arg && arg.$mid === 7) { @@ -347,7 +347,7 @@ export class ExtHostCommentThread implements vscode.CommentThread { this._commentControlHandle, this.handle ); -} + } } export class ExtHostCommentWidget implements vscode.CommentWidget { @@ -498,7 +498,7 @@ function convertFromComment(comment: modes.Comment): vscode.Comment { }; } -function convertToModeComment(vscodeComment: vscode.Comment): modes.Comment { +function convertToModeComment(vscodeComment: vscode.Comment, commandsConverter: CommandsConverter): modes.Comment { const iconPath = vscodeComment.userIconPath ? vscodeComment.userIconPath.toString() : vscodeComment.gravatar; return { @@ -506,7 +506,9 @@ function convertToModeComment(vscodeComment: vscode.Comment): modes.Comment { body: extHostTypeConverter.MarkdownString.from(vscodeComment.body), userName: vscodeComment.userName, userIconPath: iconPath, - isDraft: vscodeComment.isDraft + isDraft: vscodeComment.isDraft, + editCommand: vscodeComment.editCommand ? commandsConverter.toInternal(vscodeComment.editCommand) : undefined, + deleteCommand: vscodeComment.editCommand ? commandsConverter.toInternal(vscodeComment.deleteCommand) : undefined }; } diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts b/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts index 47862408321..e85adf64e4a 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts @@ -10,7 +10,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionsOrientation, ActionItem, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button } from 'vs/base/browser/ui/button/button'; import { Action, IActionRunner } from 'vs/base/common/actions'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -35,6 +35,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { DropdownMenuActionItem } from 'vs/base/browser/ui/dropdown/dropdown'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { ToggleReactionsAction, ReactionAction, ReactionActionItem } from './reactionsAction'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const UPDATE_COMMENT_LABEL = nls.localize('label.updateComment', "Update comment"); const UPDATE_IN_PROGRESS_LABEL = nls.localize('label.updatingComment', "Updating comment..."); @@ -51,6 +52,7 @@ export class CommentNode extends Disposable { private _reactionsActionBar?: ActionBar; private _actionsContainer?: HTMLElement; private _commentEditor: SimpleCommentEditor | null; + private _commentEditorDisposables: IDisposable[] = []; private _commentEditorModel: ITextModel; private _updateCommentButton: Button; private _errorEditingContainer: HTMLElement; @@ -67,6 +69,7 @@ export class CommentNode extends Disposable { } constructor( + private commentThread: modes.CommentThread | modes.CommentThread2, public comment: modes.Comment, private owner: string, private resource: URI, @@ -74,6 +77,7 @@ export class CommentNode extends Disposable { private themeService: IThemeService, private instantiationService: IInstantiationService, private commentService: ICommentService, + private commandService: ICommandService, private modelService: IModelService, private modeService: IModeService, private dialogService: IDialogService, @@ -130,13 +134,13 @@ export class CommentNode extends Disposable { actions.push(toggleReactionAction); } - if (this.comment.canEdit) { - this._editAction = this.createEditAction(commentDetailsContainer); + if (this.comment.canEdit || this.comment.editCommand) { + this._editAction = this.createEditAction(commentDetailsContainer, !!this.comment.editCommand); actions.push(this._editAction); } - if (this.comment.canDelete) { - this._deleteAction = this.createDeleteAction(); + if (this.comment.canDelete || this.comment.deleteCommand) { + this._deleteAction = this.createDeleteAction(!!this.comment.deleteCommand); actions.push(this._deleteAction); } @@ -300,17 +304,30 @@ export class CommentNode extends Disposable { this._commentEditor.setValue(this.comment.body.value); this._commentEditor.layout({ width: container.clientWidth - 14, height: 90 }); this._commentEditor.focus(); + + const lastLine = this._commentEditorModel.getLineCount(); const lastColumn = this._commentEditorModel.getLineContent(lastLine).length + 1; this._commentEditor.setSelection(new Selection(lastLine, lastColumn, lastLine, lastColumn)); - this._toDispose.push(this._commentEditor.onKeyDown((e: IKeyboardEvent) => { + this._commentEditorDisposables.push(this._commentEditor.onKeyDown((e: IKeyboardEvent) => { const isCmdOrCtrl = isMacintosh ? e.metaKey : e.ctrlKey; if (this._updateCommentButton.enabled && e.keyCode === KeyCode.Enter && isCmdOrCtrl) { - this.editComment(); + this.editComment(!!this.comment.editCommand); } })); + let commentThread = this.commentThread as modes.CommentThread2; + if (commentThread.commentThreadHandle) { + commentThread.input = this.comment.body.value; + this._commentEditorDisposables.push(this._commentEditor.onDidChangeModelContent(e => { + let newVal = this._commentEditor.getValue(); + if (newVal !== commentThread.input) { + commentThread.input = newVal; + } + })); + } + this._toDispose.push(this._commentEditor); this._toDispose.push(this._commentEditorModel); } @@ -320,6 +337,8 @@ export class CommentNode extends Disposable { this._body.classList.remove('hidden'); this._commentEditorModel.dispose(); + this._commentEditorDisposables.forEach(dispose => dispose.dispose()); + this._commentEditorDisposables = []; if (this._commentEditor) { this._commentEditor.dispose(); this._commentEditor = null; @@ -328,13 +347,24 @@ export class CommentNode extends Disposable { this._commentEditContainer.remove(); } - private async editComment(): Promise { + private async editComment(useCommand: boolean): Promise { this._updateCommentButton.enabled = false; this._updateCommentButton.label = UPDATE_IN_PROGRESS_LABEL; try { const newBody = this._commentEditor.getValue(); - await this.commentService.editComment(this.owner, this.resource, this.comment, newBody); + + if (useCommand) { + let commentThread = this.commentThread as modes.CommentThread2; + commentThread.input = newBody; + this.commentService.setActiveCommentThread(commentThread); + let commandId = this.comment.editCommand!.id; + let args = this.comment.editCommand!.arguments || []; + + await this.commandService.executeCommand(commandId, ...args); + } else { + await this.commentService.editComment(this.owner, this.resource, this.comment, newBody); + } this._updateCommentButton.enabled = true; this._updateCommentButton.label = UPDATE_COMMENT_LABEL; @@ -355,7 +385,7 @@ export class CommentNode extends Disposable { } } - private createDeleteAction(): Action { + private createDeleteAction(useCommand: boolean): Action { return new Action('comment.delete', nls.localize('label.delete', "Delete"), 'octicon octicon-x', true, () => { return this.dialogService.confirm({ message: nls.localize('confirmDelete', "Delete comment?"), @@ -364,11 +394,19 @@ export class CommentNode extends Disposable { }).then(async result => { if (result.confirmed) { try { - const didDelete = await this.commentService.deleteComment(this.owner, this.resource, this.comment); - if (didDelete) { - this._onDidDelete.fire(this); + if (useCommand) { + this.commentService.setActiveCommentThread(this.commentThread as modes.CommentThread2); + let commandId = this.comment.deleteCommand!.id; + let args = this.comment.deleteCommand!.arguments || []; + + await this.commandService.executeCommand(commandId, ...args); } else { - throw Error(); + const didDelete = await this.commentService.deleteComment(this.owner, this.resource, this.comment); + if (didDelete) { + this._onDidDelete.fire(this); + } else { + throw Error(); + } } } catch (e) { const error = e.message @@ -381,7 +419,7 @@ export class CommentNode extends Disposable { }); } - private createEditAction(commentDetailsContainer: HTMLElement): Action { + private createEditAction(commentDetailsContainer: HTMLElement, useCommand: boolean): Action { return new Action('comment.edit', nls.localize('label.edit', "Edit"), 'octicon octicon-pencil', true, () => { this._body.classList.add('hidden'); this._commentEditContainer = dom.append(commentDetailsContainer, dom.$('.edit-container')); @@ -403,10 +441,10 @@ export class CommentNode extends Disposable { this._toDispose.push(attachButtonStyler(this._updateCommentButton, this.themeService)); this._toDispose.push(this._updateCommentButton.onDidClick(_ => { - this.editComment(); + this.editComment(useCommand); })); - this._toDispose.push(this._commentEditor!.onDidChangeModelContent(_ => { + this._commentEditorDisposables.push(this._commentEditor!.onDidChangeModelContent(_ => { this._updateCommentButton.enabled = !!this._commentEditor!.getValue(); })); diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/electron-browser/commentThreadWidget.ts index 338a66384b3..4cdcd8125a9 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/electron-browser/commentThreadWidget.ts @@ -342,12 +342,12 @@ export class ReviewZoneWidget extends ZoneWidget { this._localToDispose.push(this._commentEditor); this._localToDispose.push(this._commentEditor.getModel().onDidChangeContent(() => this.setCommentEditorDecorations())); if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { - this._localToDispose.push(this._commentEditor.getModel().onDidChangeContent(() => { + this._localToDispose.push(this._commentEditor.getModel().onDidChangeContent(() => { let modelContent = this._commentEditor.getValue(); if ((this._commentThread as modes.CommentThread2).input !== modelContent) { (this._commentThread as modes.CommentThread2).input = modelContent; } - })); + })); this._localToDispose.push((this._commentThread as modes.CommentThread2).onDidChangeInput(input => { if (this._commentEditor.getValue() !== input) { @@ -414,7 +414,7 @@ export class ReviewZoneWidget extends ZoneWidget { this.createCommentWidgetActions2(this._formActions, model); })); } else { - this.createCommentWidgetActions(this._formActions, model); + this.createCommentWidgetActions(this._formActions, model); } this._resizeObserver = new MutationObserver(this._refresh.bind(this)); @@ -431,7 +431,7 @@ export class ReviewZoneWidget extends ZoneWidget { } // If there are no existing comments, place focus on the text area. This must be done after show, which also moves focus. - if (this._commentThread.reply && !this._commentThread.comments.length) { + if ((this._commentThread as modes.CommentThread).reply && !this._commentThread.comments.length) { this._commentEditor.focus(); } else if (this._commentEditor.getModel().getValueLength() > 0) { if (!dom.hasClass(this._commentForm, 'expand')) { @@ -564,6 +564,7 @@ export class ReviewZoneWidget extends ZoneWidget { private createNewCommentNode(comment: modes.Comment): CommentNode { let newCommentNode = new CommentNode( + this._commentThread, comment, this.owner, this.editor.getModel().uri, @@ -571,6 +572,7 @@ export class ReviewZoneWidget extends ZoneWidget { this.themeService, this.instantiationService, this.commentService, + this.commandService, this.modelService, this.modeService, this.dialogService, @@ -676,7 +678,7 @@ export class ReviewZoneWidget extends ZoneWidget { if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { // this._reviewThreadReplyButton.title = (this._commentThread as modes.CommentThread2).acceptInputCommands.title; } else { - this._reviewThreadReplyButton.title = nls.localize('reply', "Reply..."); + this._reviewThreadReplyButton.title = nls.localize('reply', "Reply..."); } this._reviewThreadReplyButton.textContent = nls.localize('reply', "Reply..."); // bind click/escape actions for reviewThreadReplyButton and textArea