提交 130fdbee 编写于 作者: P Peng Lyu

reaction api

上级 057b48ef
......@@ -1229,6 +1229,14 @@ export interface NewCommentAction {
actions: Command[];
}
/**
* @internal
*/
export interface CommentReaction {
readonly label?: string;
readonly hasReacted?: boolean;
}
/**
* @internal
*/
......@@ -1241,6 +1249,7 @@ export interface Comment {
readonly canDelete?: boolean;
readonly command?: Command;
readonly isDraft?: boolean;
readonly commentReactions?: CommentReaction[];
}
/**
......@@ -1284,6 +1293,11 @@ export interface DocumentCommentProvider {
startDraftLabel?: string;
deleteDraftLabel?: string;
finishDraftLabel?: string;
addReaction?(resource: URI, comment: Comment, reaction: CommentReaction, token: CancellationToken): Promise<void>;
deleteReaction?(resource: URI, comment: Comment, reaction: CommentReaction, token: CancellationToken): Promise<void>;
reactionGroup?: CommentReaction[];
onDidChangeCommentThreads(): Event<CommentThreadChangedEvent>;
}
......
......@@ -820,7 +820,7 @@ declare module 'vscode' {
interface CommentReaction {
readonly label?: string;
readonly iconPath?: string | Uri;
readonly hasReacted?: boolean;
}
interface DocumentCommentProvider {
......@@ -857,10 +857,9 @@ declare module 'vscode' {
deleteDraftLabel?: string;
finishDraftLabel?: string;
commentReactions?: CommentReaction[];
addReaction(comment: Comment, reaction: CommentReaction): Promise<void>;
deleteReaction(comment: Comment, reaction: CommentReaction): Promise<void>;
addReaction?(document: TextDocument, comment: Comment, reaction: CommentReaction): Promise<void>;
deleteReaction?(document: TextDocument, comment: Comment, reaction: CommentReaction): Promise<void>;
reactionGroup?: CommentReaction[];
/**
* Notify of updates to comment threads.
......
......@@ -30,6 +30,7 @@ export class MainThreadDocumentCommentProvider implements modes.DocumentCommentP
get startDraftLabel(): string { return this._features.startDraftLabel; }
get deleteDraftLabel(): string { return this._features.deleteDraftLabel; }
get finishDraftLabel(): string { return this._features.finishDraftLabel; }
get reactionGroup(): modes.CommentReaction[] { return this._features.reactionGroup; }
constructor(proxy: ExtHostCommentsShape, handle: number, features: CommentProviderFeatures) {
this._proxy = proxy;
......@@ -66,6 +67,13 @@ export class MainThreadDocumentCommentProvider implements modes.DocumentCommentP
async finishDraft(uri, token): Promise<void> {
return this._proxy.$finishDraft(this._handle, uri);
}
async addReaction(uri, comment: modes.Comment, reaction: modes.CommentReaction, token): Promise<void> {
return this._proxy.$addReaction(this._handle, uri, comment, reaction);
}
async deleteReaction(uri, comment: modes.Comment, reaction: modes.CommentReaction, token): Promise<void> {
return this._proxy.$deleteReaction(this._handle, uri, comment, reaction);
}
onDidChangeCommentThreads = null;
}
......
......@@ -110,6 +110,7 @@ export interface CommentProviderFeatures {
startDraftLabel?: string;
deleteDraftLabel?: string;
finishDraftLabel?: string;
reactionGroup?: vscode.CommentReaction[];
}
export interface MainThreadCommentsShape extends IDisposable {
......@@ -1063,6 +1064,8 @@ export interface ExtHostCommentsShape {
$startDraft(handle: number, document: UriComponents): Promise<void>;
$deleteDraft(handle: number, document: UriComponents): Promise<void>;
$finishDraft(handle: number, document: UriComponents): Promise<void>;
$addReaction(handle: number, document: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise<void>;
$deleteReaction(handle: number, document: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise<void>;
$provideWorkspaceComments(handle: number): Promise<modes.CommentThread[]>;
}
......
......@@ -69,7 +69,8 @@ export class ExtHostComments implements ExtHostCommentsShape {
this._proxy.$registerDocumentCommentProvider(handle, {
startDraftLabel: provider.startDraftLabel,
deleteDraftLabel: provider.deleteDraftLabel,
finishDraftLabel: provider.finishDraftLabel
finishDraftLabel: provider.finishDraftLabel,
reactionGroup: provider.reactionGroup
});
this.registerListeners(handle, extensionId, provider);
......@@ -174,6 +175,34 @@ export class ExtHostComments implements ExtHostCommentsShape {
});
}
$addReaction(handle: number, uri: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise<void> {
const data = this._documents.getDocumentData(URI.revive(uri));
if (!data || !data.document) {
throw new Error('Unable to retrieve document from URI');
}
const handlerData = this._documentProviders.get(handle);
return asPromise(() => {
return handlerData.provider.addReaction(data.document, convertFromComment(comment), reaction);
});
}
$deleteReaction(handle: number, uri: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise<void> {
const data = this._documents.getDocumentData(URI.revive(uri));
if (!data || !data.document) {
throw new Error('Unable to retrieve document from URI');
}
const handlerData = this._documentProviders.get(handle);
return asPromise(() => {
return handlerData.provider.deleteReaction(data.document, convertFromComment(comment), reaction);
});
}
$provideDocumentComments(handle: number, uri: UriComponents): Promise<modes.CommentInfo> {
const data = this._documents.getDocumentData(URI.revive(uri));
if (!data || !data.document) {
......@@ -259,7 +288,8 @@ function convertFromComment(comment: modes.Comment): vscode.Comment {
userIconPath: userIconPath,
canEdit: comment.canEdit,
canDelete: comment.canDelete,
isDraft: comment.isDraft
isDraft: comment.isDraft,
commentReactions: comment.commentReactions
};
}
......@@ -275,6 +305,7 @@ function convertToComment(provider: vscode.DocumentCommentProvider | vscode.Work
canEdit: canEdit,
canDelete: canDelete,
command: vscodeComment.command ? commandsConverter.toInternal(vscodeComment.command) : null,
isDraft: vscodeComment.isDraft
isDraft: vscodeComment.isDraft,
commentReactions: vscodeComment.commentReactions
};
}
......@@ -7,9 +7,9 @@ import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import * as modes from 'vs/editor/common/modes';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { ActionsOrientation, ActionItem, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Button } from 'vs/base/browser/ui/button/button';
import { Action } from 'vs/base/common/actions';
import { Action, IActionRunner } from 'vs/base/common/actions';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ITextModel } from 'vs/editor/common/model';
......@@ -30,6 +30,8 @@ import { Emitter, Event } from 'vs/base/common/event';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { assign } from 'vs/base/common/objects';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
const UPDATE_COMMENT_LABEL = nls.localize('label.updateComment', "Update comment");
const UPDATE_IN_PROGRESS_LABEL = nls.localize('label.updatingComment', "Updating comment...");
......@@ -49,6 +51,9 @@ export class CommentNode extends Disposable {
private _isPendingLabel: HTMLElement;
private _deleteAction: Action;
protected actionRunner?: IActionRunner;
protected toolbar: ToolBar;
private _onDidDelete = new Emitter<CommentNode>();
public get domNode(): HTMLElement {
......@@ -66,7 +71,8 @@ export class CommentNode extends Disposable {
private modelService: IModelService,
private modeService: IModeService,
private dialogService: IDialogService,
private notificationService: INotificationService
private notificationService: INotificationService,
private contextMenuService: IContextMenuService
) {
super();
......@@ -86,7 +92,9 @@ export class CommentNode extends Disposable {
this._md = this.markdownRenderer.render(comment.body).element;
this._body.appendChild(this._md);
this.createReactions(commentDetailsContainer);
if (this.comment.commentReactions) {
this.createReactions(commentDetailsContainer);
}
this._domNode.setAttribute('aria-label', `${comment.userName}, ${comment.body.value}`);
this._domNode.setAttribute('role', 'treeitem');
......@@ -121,23 +129,79 @@ export class CommentNode extends Disposable {
if (actions.length) {
const actionsContainer = dom.append(header, dom.$('.comment-actions.hidden'));
const actionBar = new ActionBar(actionsContainer, {});
this._toDispose.push(actionBar);
this.toolbar = new ToolBar(actionsContainer, this.contextMenuService, {
actionItemProvider: action => this.actionItemProvider(action as Action),
orientation: ActionsOrientation.HORIZONTAL
});
this.registerActionBarListeners(actionsContainer);
actions.forEach(action => actionBar.push(action, { label: false, icon: true }));
let reactionActions = [];
let reactionGroup = this.commentService.getReactionGroup(this.owner);
if (reactionGroup) {
reactionActions = reactionGroup.map((reaction) => {
return new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => {
try {
await this.commentService.addReaction(this.owner, this.resource, this.comment, reaction);
} catch (e) {
const error = e.message
? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message)
: nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed");
this.notificationService.error(error);
}
});
});
}
this.toolbar.setActions(actions, reactionActions)();
this._toDispose.push(this.toolbar);
}
}
private createReactions(commentDetailsContainer: HTMLElement): void {
let reactions = ['❤️', '🎉', '😄'];
actionItemProvider(action: Action) {
let options = {};
if (action.id === 'comment.delete' || action.id === 'comment.edit') {
options = { label: false, icon: true };
} else {
options = { label: true, icon: true };
}
const reactionsBar = dom.append(commentDetailsContainer, dom.$('div.comment-reactions'));
let item = new ActionItem({}, action, options);
return item;
}
reactions.forEach(reaction => {
let btn = new Button(reactionsBar);
btn.label = reaction;
private createReactions(commentDetailsContainer: HTMLElement): void {
const actionsContainer = dom.append(commentDetailsContainer, dom.$('div.comment-reactions'));
const actionBar = new ActionBar(actionsContainer, {});
this._toDispose.push(actionBar);
let reactionActions = this.comment.commentReactions.map(reaction => {
return new Action(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted ? 'active' : '', true, async () => {
try {
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;
if (reaction.hasReacted) {
error = e.message
? nls.localize('commentDeleteReactionError', "Deleting the comment reaction failed: {0}.", e.message)
: nls.localize('commentDeleteReactionDefaultError', "Deleting the comment reaction failed");
} else {
error = e.message
? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message)
: nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed");
}
this.notificationService.error(error);
}
});
});
reactionActions.forEach(action => actionBar.push(action, { label: true, icon: true }));
}
private createCommentEditor(): void {
......@@ -272,7 +336,7 @@ export class CommentNode extends Disposable {
actionsContainer.classList.remove('hidden');
}));
this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseleave', (e: MouseEvent) => {
this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseleave', () => {
if (!this._domNode.contains(document.activeElement)) {
actionsContainer.classList.add('hidden');
}
......
......@@ -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 } from 'vs/editor/common/modes';
import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction } 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';
......@@ -55,6 +55,9 @@ export interface ICommentService {
getStartDraftLabel(owner: string): string;
getDeleteDraftLabel(owner: string): string;
getFinishDraftLabel(owner: string): string;
addReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void>;
deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void>;
getReactionGroup(owner: string): CommentReaction[];
}
export class CommentService extends Disposable implements ICommentService {
......@@ -178,6 +181,36 @@ export class CommentService extends Disposable implements ICommentService {
}
}
async addReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void> {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider && commentProvider.addReaction) {
return commentProvider.addReaction(resource, comment, reaction, CancellationToken.None);
} else {
throw new Error('Not supported');
}
}
async deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise<void> {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider && commentProvider.deleteReaction) {
return commentProvider.deleteReaction(resource, comment, reaction, CancellationToken.None);
} else {
throw new Error('Not supported');
}
}
getReactionGroup(owner: string): CommentReaction[] {
const commentProvider = this._commentProviders.get(owner);
if (commentProvider) {
return commentProvider.reactionGroup;
}
return null;
}
getStartDraftLabel(owner: string): string | null {
const commentProvider = this._commentProviders.get(owner);
......
......@@ -39,6 +39,7 @@ import { CommentNode } from 'vs/workbench/parts/comments/electron-browser/commen
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ITextModel } from 'vs/editor/common/model';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration';
const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-x';
......@@ -97,6 +98,7 @@ export class ReviewZoneWidget extends ZoneWidget {
private openerService: IOpenerService,
private dialogService: IDialogService,
private notificationService: INotificationService,
private contextMenuService: IContextMenuService,
editor: ICodeEditor,
owner: string,
commentThread: modes.CommentThread,
......@@ -491,7 +493,8 @@ export class ReviewZoneWidget extends ZoneWidget {
this.modelService,
this.modeService,
this.dialogService,
this.notificationService);
this.notificationService,
this.contextMenuService);
this._disposables.push(newCommentNode);
this._disposables.push(newCommentNode.onDidDelete(deletedNode => {
......
......@@ -35,6 +35,8 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { overviewRulerCommentingRangeForeground } from 'vs/workbench/parts/comments/electron-browser/commentGlyphWidget';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme';
export const ctxReviewPanelVisible = new RawContextKey<boolean>('reviewPanelVisible', false);
......@@ -175,7 +177,8 @@ export class ReviewController implements IEditorContribution {
@IModelService private readonly modelService: IModelService,
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
@IOpenerService private readonly openerService: IOpenerService,
@IDialogService private readonly dialogService: IDialogService
@IDialogService private readonly dialogService: IDialogService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
) {
this.editor = editor;
this.globalToDispose = [];
......@@ -393,7 +396,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.dialogService, this.notificationService, this.editor, e.owner, thread, null, draftMode, {});
let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.notificationService, this.contextMenuService, this.editor, e.owner, thread, null, draftMode, {});
zoneWidget.display(thread.range.startLineNumber);
this._commentWidgets.push(zoneWidget);
this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread);
......@@ -412,7 +415,7 @@ export class ReviewController implements IEditorContribution {
// add new comment
this._reviewPanelVisible.set(true);
this._newCommentWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.notificationService, this.editor, ownerId, {
this._newCommentWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.notificationService, this.contextMenuService, this.editor, ownerId, {
extensionId: extensionId,
threadId: null,
resource: null,
......@@ -570,7 +573,7 @@ export class ReviewController implements IEditorContribution {
thread.collapsibleState = modes.CommentThreadCollapsibleState.Expanded;
}
let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.notificationService, this.editor, info.owner, thread, pendingComment, info.draftMode, {});
let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.themeService, this.commentService, this.openerService, this.dialogService, this.notificationService, this.contextMenuService, this.editor, info.owner, thread, pendingComment, info.draftMode, {});
zoneWidget.display(thread.range.startLineNumber);
this._commentWidgets.push(zoneWidget);
});
......@@ -741,4 +744,16 @@ registerThemingParticipant((theme, collector) => {
}
`);
}
const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND);
if (statusBarItemHoverBackground) {
collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label:hover { background-color: ${statusBarItemHoverBackground}; border: 1px solid grey;
border-radius: 3px; }`);
}
const statusBarItemActiveBackground = theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND);
if (statusBarItemActiveBackground) {
collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label:active { background-color: ${statusBarItemActiveBackground}; border: 1px solid grey;
border-radius: 3px;}`);
}
});
......@@ -56,6 +56,13 @@
line-height: 18px;
}
.monaco-editor .review-widget .body .review-comment .comment-title .monaco-dropdown .toolbar-toggle-more {
width: 16px;
height: 18px;
line-height: 18px;
vertical-align: middle;
}
.monaco-editor .review-widget .body .comment-body blockquote {
margin: 0 7px 0 5px;
padding: 0 16px 0 10px;
......@@ -120,6 +127,32 @@
padding-top: 4px;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions {
margin-top: 8px;
min-height: 25px;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .monaco-action-bar .actions-container {
justify-content: flex-start;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label {
padding: 2px 5px 0px 5px;
white-space: pre;
text-align: center;
font-size: 14px;
margin: 4px;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active {
border: 1px solid grey;
border-radius: 3px;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.disabled {
opacity: 0.6;
}
.monaco-editor.vs-dark .review-widget .body span.created_at {
color: #e0e0e0;
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册