diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 5a88b126fd8..b57455215b5 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -809,7 +809,7 @@ declare module 'vscode' { /** * A comment is displayed within the editor or the Comments Panel, depending on how it is provided. */ - interface Comment { + export interface Comment { /** * The id of the comment */ @@ -983,6 +983,11 @@ declare module 'vscode' { provideCommentingRanges(document: TextDocument, token: CancellationToken): ProviderResult; } + export interface CommentReactionProvider { + availableReactions: CommentReaction[]; + toggleReaction?(document: TextDocument, comment: Comment, reaction: CommentReaction): Promise; + } + export interface EmptyCommentThreadFactory { /** * The method `createEmptyCommentThread` is called when users attempt to create new comment thread from the gutter or command palette. @@ -1019,6 +1024,11 @@ declare module 'vscode' { */ emptyCommentThreadFactory: EmptyCommentThreadFactory; + /** + * Optional reaction provider + */ + reactionProvider?: CommentReactionProvider; + /** * Dispose this comment controller. */ diff --git a/src/vs/workbench/api/electron-browser/mainThreadComments.ts b/src/vs/workbench/api/electron-browser/mainThreadComments.ts index 716ab253251..4b21dfb2027 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadComments.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadComments.ts @@ -212,17 +212,34 @@ export class MainThreadCommentController { return this._label; } + private _reactions: modes.CommentReaction[] | undefined; + + get reactions() { + return this._reactions; + } + + set reactions(reactions: modes.CommentReaction[] | undefined) { + this._reactions = reactions; + } + private readonly _threads: Map = new Map(); public activeCommentThread?: MainThreadCommentThread; + + constructor( private readonly _proxy: ExtHostCommentsShape, private readonly _commentService: ICommentService, private readonly _handle: number, private readonly _uniqueId: string, private readonly _id: string, - private readonly _label: string + private readonly _label: string, + private _features: CommentProviderFeatures ) { } + updateFeatures(features: CommentProviderFeatures) { + this._features = features; + } + createCommentThread(commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], collapseState: modes.CommentThreadCollapsibleState): modes.CommentThread2 { let thread = new MainThreadCommentThread( commentThreadHandle, @@ -347,6 +364,14 @@ export class MainThreadCommentController { return commentingRanges || []; } + getReactionGroup(): modes.CommentReaction[] | undefined { + return this._features.reactionGroup; + } + + async toggleReaction(uri, thread: modes.CommentThread2, comment: modes.Comment, reaction: modes.CommentReaction, token): Promise { + return this._proxy.$toggleReaction(this._handle, thread.commentThreadHandle, uri, comment, reaction); + } + getAllComments(): MainThreadCommentThread[] { let ret: MainThreadCommentThread[] = []; for (let thread of keys(this._threads)) { @@ -414,8 +439,8 @@ export class MainThreadComments extends Disposable implements MainThreadComments const providerId = generateUuid(); this._handlers.set(handle, providerId); - const provider = new MainThreadCommentController(this._proxy, this._commentService, handle, providerId, id, label); - this._commentService.registerCommentController(String(handle), provider); + const provider = new MainThreadCommentController(this._proxy, this._commentService, handle, providerId, id, label, {}); + this._commentService.registerCommentController(providerId, provider); this._commentControllers.set(handle, provider); const commentsPanelAlreadyConstructed = this._panelService.getPanels().some(panel => panel.id === COMMENTS_PANEL_ID); @@ -426,6 +451,16 @@ export class MainThreadComments extends Disposable implements MainThreadComments this._commentService.setWorkspaceComments(String(handle), []); } + $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void { + let provider = this._commentControllers.get(handle); + + if (!provider) { + return undefined; + } + + provider.updateFeatures(features); + } + $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], collapseState: modes.CommentThreadCollapsibleState): modes.CommentThread2 | undefined { let provider = this._commentControllers.get(handle); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 1b02d7c3cc8..16ae7934138 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -120,6 +120,7 @@ export interface CommentProviderFeatures { export interface MainThreadCommentsShape extends IDisposable { $registerCommentController(handle: number, id: string, label: string): void; + $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void; $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], collapseState: modes.CommentThreadCollapsibleState): modes.CommentThread2 | undefined; $deleteCommentThread(handle: number, commentThreadHandle: number): void; $updateComments(handle: number, commentThreadHandle: number, comments: modes.Comment[]): void; @@ -1108,6 +1109,8 @@ export interface ExtHostCommentsShape { $createNewCommentThread(handle: number, document: UriComponents, range: IRange, text: string): Promise; $onCommentWidgetInputChange(commentControllerHandle: number, input: string | undefined): Promise; $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise; + $provideReactionGroup(commentControllerHandle: number): Promise; + $toggleReaction(commentControllerHandle: number, threadHandle: number, uri: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise; $createNewCommentWidgetCallback(commentControllerHandle: number, uriComponents: UriComponents, range: IRange, token: CancellationToken): void; $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 60576f39a01..b93df241a3f 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -112,6 +112,37 @@ export class ExtHostComments implements ExtHostCommentsShape { }).then(ranges => ranges ? ranges.map(x => extHostTypeConverter.Range.from(x)) : undefined); } + $provideReactionGroup(commentControllerHandle: number): Promise { + const commentController = this._commentControllers.get(commentControllerHandle); + + if (!commentController || !commentController.reactionProvider) { + return Promise.resolve(undefined); + } + + return asPromise(() => { + return commentController.reactionProvider.availableReactions; + }).then(reactions => reactions.map(reaction => convertToReaction2(commentController.reactionProvider, reaction))); + } + + $toggleReaction(commentControllerHandle: number, threadHandle: number, uri: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise { + const document = this._documents.getDocument(URI.revive(uri)); + const commentController = this._commentControllers.get(commentControllerHandle); + + if (!commentController || !commentController.reactionProvider || !commentController.reactionProvider.toggleReaction) { + return Promise.resolve(undefined); + } + + return asPromise(() => { + const commentThread = commentController.getCommentThread(threadHandle); + if (commentThread) { + const vscodeComment = commentThread.getComment(comment.commentId); + return commentController.reactionProvider.toggleReaction(document, vscodeComment, convertFromReaction(reaction)); + } + + return Promise.resolve(undefined); + }); + } + $createNewCommentWidgetCallback(commentControllerHandle: number, uriComponents: UriComponents, range: IRange, token: CancellationToken): void { const commentController = this._commentControllers.get(commentControllerHandle); @@ -485,6 +516,18 @@ class ExtHostCommentController implements vscode.CommentController { commentingRangeProvider?: vscode.CommentingRangeProvider; emptyCommentThreadFactory: vscode.EmptyCommentThreadFactory; + + private _commentReactionProvider?: vscode.CommentReactionProvider; + + get reactionProvider() { + return this._commentReactionProvider; + } + + set reactionProvider(provider: vscode.CommentReactionProvider) { + this._commentReactionProvider = provider; + this._proxy.$updateCommentControllerFeatures(this.handle, { reactionGroup: provider.availableReactions.map(reaction => convertToReaction2(provider, reaction)) }); + } + constructor( _extension: IExtensionDescription, private _handle: number, @@ -591,7 +634,8 @@ function convertToModeComment(vscodeComment: vscode.Comment, commandsConverter: selectCommand: vscodeComment.selectCommand ? commandsConverter.toInternal(vscodeComment.selectCommand) : undefined, editCommand: vscodeComment.editCommand ? commandsConverter.toInternal(vscodeComment.editCommand) : undefined, deleteCommand: vscodeComment.editCommand ? commandsConverter.toInternal(vscodeComment.deleteCommand) : undefined, - label: vscodeComment.label + label: vscodeComment.label, + commentReactions: vscodeComment.commentReactions ? vscodeComment.commentReactions.map(reaction => convertToReaction2(undefined, reaction)) : undefined }; } @@ -627,6 +671,16 @@ function convertToReaction(provider: vscode.DocumentCommentProvider | vscode.Wor }; } +function convertToReaction2(provider: vscode.CommentReactionProvider | undefined, reaction: vscode.CommentReaction): modes.CommentReaction { + return { + label: reaction.label, + iconPath: reaction.iconPath ? extHostTypeConverter.pathOrURIToURI(reaction.iconPath) : undefined, + count: reaction.count, + hasReacted: reaction.hasReacted, + canEdit: true//!!provider.toggleReaction + }; +} + function convertFromReaction(reaction: modes.CommentReaction): vscode.CommentReaction { return { label: reaction.label, diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts b/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts index 1b1761fab59..0baa989fb19 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts @@ -133,8 +133,14 @@ export class CommentNode extends Disposable { let reactionGroup = this.commentService.getReactionGroup(this.owner); if (reactionGroup && reactionGroup.length) { - let toggleReactionAction = this.createReactionPicker(); - actions.push(toggleReactionAction); + let commentThread = this.commentThread as modes.CommentThread2; + if (commentThread.commentThreadHandle) { + let toggleReactionAction = this.createReactionPicker2(); + actions.push(toggleReactionAction); + } else { + let toggleReactionAction = this.createReactionPicker(); + actions.push(toggleReactionAction); + } } if (this.comment.canEdit || this.comment.editCommand) { @@ -194,6 +200,52 @@ export class CommentNode extends Disposable { } } + private createReactionPicker2(): ToggleReactionsAction { + let toggleReactionActionItem: DropdownMenuActionItem; + let toggleReactionAction = this._register(new ToggleReactionsAction(() => { + if (toggleReactionActionItem) { + toggleReactionActionItem.show(); + } + }, nls.localize('commentToggleReaction', "Toggle Reaction"))); + + let reactionMenuActions: Action[] = []; + let reactionGroup = this.commentService.getReactionGroup(this.owner); + if (reactionGroup && reactionGroup.length) { + reactionMenuActions = reactionGroup.map((reaction) => { + return new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => { + try { + await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread as modes.CommentThread2, this.comment, reaction); + } catch (e) { + const error = e.message + ? nls.localize('commentToggleReactionError', "Toggling the comment reaction failed: {0}.", e.message) + : nls.localize('commentToggleReactionDefaultError', "Toggling the comment reaction failed"); + this.notificationService.error(error); + } + }); + }); + } + + toggleReactionAction.menuActions = reactionMenuActions; + + toggleReactionActionItem = new DropdownMenuActionItem( + toggleReactionAction, + (toggleReactionAction).menuActions, + this.contextMenuService, + action => { + if (action.id === ToggleReactionsAction.ID) { + return toggleReactionActionItem; + } + return this.actionItemProvider(action as Action); + }, + this.actionRunner, + undefined, + 'toolbar-toggle-pickReactions', + () => { return AnchorAlignment.RIGHT; } + ); + + return toggleReactionAction; + } + private createReactionPicker(): ToggleReactionsAction { let toggleReactionActionItem: DropdownMenuActionItem; let toggleReactionAction = this._register(new ToggleReactionsAction(() => { @@ -266,10 +318,15 @@ export class CommentNode extends Disposable { this.comment.commentReactions!.map(reaction => { let action = new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && reaction.canEdit ? 'active' : '', reaction.canEdit, async () => { try { - if (reaction.hasReacted) { - await this.commentService.deleteReaction(this.owner, this.resource, this.comment, reaction); + let commentThread = this.commentThread as modes.CommentThread2; + if (commentThread.commentThreadHandle) { + await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread as modes.CommentThread2, this.comment, reaction); } else { - await this.commentService.addReaction(this.owner, this.resource, this.comment, reaction); + if (reaction.hasReacted) { + await this.commentService.deleteReaction(this.owner, this.resource, this.comment, reaction); + } else { + await this.commentService.addReaction(this.owner, this.resource, this.comment, reaction); + } } } catch (e) { let error: string; @@ -294,8 +351,14 @@ export class CommentNode extends Disposable { let reactionGroup = this.commentService.getReactionGroup(this.owner); if (reactionGroup && reactionGroup.length) { - let toggleReactionAction = this.createReactionPicker(); - this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); + let commentThread = this.commentThread as modes.CommentThread2; + if (commentThread.commentThreadHandle) { + let toggleReactionAction = this.createReactionPicker2(); + this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); + } else { + let toggleReactionAction = this.createReactionPicker(); + this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); + } } } diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentService.ts b/src/vs/workbench/contrib/comments/electron-browser/commentService.ts index 32f7d41217c..61db44b3a09 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/electron-browser/commentService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction, CommentingRanges } from 'vs/editor/common/modes'; +import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction, CommentingRanges, CommentThread2 } from 'vs/editor/common/modes'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -63,6 +63,7 @@ export interface ICommentService { addReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise; deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise; getReactionGroup(owner: string): CommentReaction[] | undefined; + toggleReaction(owner: string, resource: URI, thread: CommentThread2, comment: Comment, reaction: CommentReaction): Promise; setActiveCommentThread(commentThread: CommentThread | null); setInput(input: string); } @@ -237,11 +238,27 @@ export class CommentService extends Disposable implements ICommentService { } } + async toggleReaction(owner: string, resource: URI, thread: CommentThread2, comment: Comment, reaction: CommentReaction): Promise { + const commentController = this._commentControls.get(owner); + + if (commentController) { + return commentController.toggleReaction(resource, thread, comment, reaction, CancellationToken.None); + } else { + throw new Error('Not supported'); + } + } + getReactionGroup(owner: string): CommentReaction[] | undefined { - const commentProvider = this._commentProviders.get(owner); + const commentProvider = this._commentControls.get(owner); if (commentProvider) { - return commentProvider.reactionGroup; + return commentProvider.getReactionGroup(); + } + + const commentController = this._commentControls.get(owner); + + if (commentController) { + return commentController.getReactionGroup(); } return undefined; diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/electron-browser/commentThreadWidget.ts index f8ebe7fd893..51f8a5f0909 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/electron-browser/commentThreadWidget.ts @@ -231,7 +231,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } - update(commentThread: modes.CommentThread | modes.CommentThread2) { + async update(commentThread: modes.CommentThread | modes.CommentThread2) { const oldCommentsLen = this._commentElements.length; const newCommentsLen = commentThread.comments.length; @@ -400,8 +400,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } })); - this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeComments(_ => { - this.update(this._commentThread); + this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeComments(async _ => { + await this.update(this._commentThread); })); this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeLabel(_ => {