diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 082dcf810f6..90833069146 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1288,6 +1288,7 @@ export interface CommentThread2 { resource: string | null; range: IRange; label: string; + contextValue: string | undefined; comments: Comment[] | undefined; onDidChangeComments: Event; collapsibleState?: CommentThreadCollapsibleState; @@ -1326,6 +1327,7 @@ export interface CommentThread { collapsibleState?: CommentThreadCollapsibleState; reply?: Command; isDisposed?: boolean; + contextValue?: string; } /** @@ -1347,14 +1349,24 @@ export interface CommentReaction { readonly canEdit?: boolean; } +/** + * @internal + */ +export enum CommentMode { + Editing = 0, + Preview = 1 +} + /** * @internal */ export interface Comment { readonly commentId: string; + readonly uniqueIdInThread?: number; readonly body: IMarkdownString; readonly userName: string; readonly userIconPath?: string; + readonly contextValue?: string; readonly canEdit?: boolean; readonly canDelete?: boolean; readonly selectCommand?: Command; @@ -1363,6 +1375,7 @@ export interface Comment { readonly isDraft?: boolean; readonly commentReactions?: CommentReaction[]; readonly label?: string; + readonly mode?: CommentMode; } /** diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 8aacb21dfb2..d0290c179fd 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -95,6 +95,10 @@ export const enum MenuId { TouchBarContext, ViewItemContext, ViewTitle, + CommentThreadTitle, + CommentThreadActions, + CommentTitle, + CommentActions } export interface IMenuActionOptions { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index a4bf1796b7d..dafc98abc04 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -8940,6 +8940,237 @@ declare module 'vscode' { */ export const onDidChange: Event; } + + //#region Comments + + /** + * Collapsible state of a [comment thread](#CommentThread) + */ + export enum CommentThreadCollapsibleState { + /** + * Determines an item is collapsed + */ + Collapsed = 0, + + /** + * Determines an item is expanded + */ + Expanded = 1 + } + + /** + * Comment mode of a [comment](#Comment) + */ + export enum CommentMode { + /** + * Displays the comment editor + */ + Editing = 0, + + /** + * Displays the preview of the comment + */ + Preview = 1 + } + + /** + * A collection of [comments](#Comment) representing a conversation at a particular range in a document. + */ + export interface CommentThread { + /** + * The uri of the document the thread has been created on. + */ + readonly resource: Uri; + + /** + * The range the comment thread is located within the document. The thread icon will be shown + * at the first line of the range. + */ + range: Range; + + /** + * The ordered comments of the thread. + */ + comments: ReadonlyArray; + + /** + * Whether the thread should be collapsed or expanded when opening the document. + * Defaults to Collapsed. + */ + collapsibleState: CommentThreadCollapsibleState; + + /** + * Context value of the comment thread. This can be used to contribute thread specific actions. + * For example, a comment thread is given a context value as `editable`. When contributing actions to `comments/commentThread/title` + * using `menus` extension point, you can specify context value for key `commentThread` in `when` expression like `commentThread == editable`. + * ``` + * "contributes": { + * "menus": { + * "comments/commentThread/title": [ + * { + * "command": "extension.deleteCommentThread", + * "when": "commentThread == editable" + * } + * ] + * } + * } + * ``` + * This will show action `extension.deleteCommentThread` only for comment threads with `contextValue` is `editable`. + */ + contextValue?: string; + + /** + * The optional human-readable label describing the [Comment Thread](#CommentThread) + */ + label?: string; + + /** + * Dispose this comment thread. + * + * Once disposed, this comment thread will be removed from visible editors and Comment Panel when approriate. + */ + dispose(): void; + } + + /** + * Author information of a [comment](#Comment) + */ + export interface CommentAuthorInformation { + /** + * The display name of the author of the comment + */ + name: string; + + /** + * The optional icon path for the author + */ + iconPath?: Uri; + } + + /** + * A comment is displayed within the editor or the Comments Panel, depending on how it is provided. + */ + export interface Comment { + /** + * The human-readable comment body + */ + body: string | MarkdownString; + + /** + * [Comment mode](#CommentMode) of the comment + */ + mode: CommentMode; + + /** + * The [author information](#CommentAuthorInformation) of the comment + */ + author: CommentAuthorInformation; + + /** + * Context value of the comment. This can be used to contribute comment specific actions. + * For example, a comment is given a context value as `editable`. When contributing actions to `comments/comment/title` + * using `menus` extension point, you can specify context value for key `comment` in `when` expression like `comment == editable`. + * ``` + * "contributes": { + * "menus": { + * "comments/comment/title": [ + * { + * "command": "extension.deleteComment", + * "when": "comment == editable" + * } + * ] + * } + * } + * ``` + * This will show action `extension.deleteComment` only for comments with `contextValue` is `editable`. + */ + contextValue?: string; + + /** + * Optional label describing the [Comment](#Comment) + * Label will be rendered next to authorName if exists. + */ + label?: string; + } + + /** + * Command argument for actions registered in `comments/commentThread/actions`. + */ + export interface CommentReply { + /** + * The active [comment thread](#CommentThread) + */ + thread: CommentThread; + + /** + * The value in the comment editor + */ + text: string; + } + + /** + * Commenting range provider for a [comment controller](#CommentController). + */ + export interface CommentingRangeProvider { + /** + * Provide a list of ranges which allow new comment threads creation or null for a given document + */ + provideCommentingRanges(document: TextDocument, token: CancellationToken): ProviderResult; + } + + /** + * A comment controller is able to provide [comments](#CommentThread) support to the editor and + * provide users various ways to interact with comments. + */ + export interface CommentController { + /** + * The id of this comment controller. + */ + readonly id: string; + + /** + * The human-readable label of this comment controller. + */ + readonly label: string; + + /** + * Optional commenting range provider. Provide a list [ranges](#Range) which support commenting to any given resource uri. + * + * If not provided, users can leave comments in any document opened in the editor. + */ + commentingRangeProvider?: CommentingRangeProvider; + + /** + * Create a [comment thread](#CommentThread). The comment thread will be displayed in visible text editors (if the resource matches) + * and Comments Panel once created. + * + * @param resource The uri of the document the thread has been created on. + * @param range The range the comment thread is located within the document. + * @param comments The ordered comments of the thread. + */ + createCommentThread(uri: Uri, range: Range, comments: Comment[]): CommentThread; + + /** + * Dispose this comment controller. + * + * Once disposed, all [comment threads](#CommentThread) created by this comment controller will also be removed from the editor + * and Comments Panel. + */ + dispose(): void; + } + + namespace comments { + /** + * Creates a new [comment controller](#CommentController) instance. + * + * @param id An `id` for the comment controller. + * @param label A human-readable string for the comment controller. + * @return An instance of [comment controller](#CommentController). + */ + export function createCommentController(id: string, label: string): CommentController; + } + + //#endregion } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index f92a6f746cc..c08a3725b07 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -957,15 +957,6 @@ declare module 'vscode' { reactionProvider?: CommentReactionProvider; } - export interface CommentController { - /** - * The active [comment thread](#CommentThread) or `undefined`. The `activeCommentThread` is the comment thread of - * the comment widget that currently has focus. It's `undefined` when the focus is not in any comment thread widget, or - * the comment widget created from [comment thread template](#CommentThreadTemplate). - */ - readonly activeCommentThread: CommentThread | undefined; - } - namespace workspace { /** * DEPRECATED @@ -979,21 +970,6 @@ declare module 'vscode' { export function registerWorkspaceCommentProvider(provider: WorkspaceCommentProvider): Disposable; } - /** - * Collapsible state of a [comment thread](#CommentThread) - */ - export enum CommentThreadCollapsibleState { - /** - * Determines an item is collapsed - */ - Collapsed = 0, - - /** - * Determines an item is expanded - */ - Expanded = 1 - } - /** * A collection of [comments](#Comment) representing a conversation at a particular range in a document. */ @@ -1008,28 +984,6 @@ declare module 'vscode' { */ readonly uri: Uri; - /** - * The range the comment thread is located within the document. The thread icon will be shown - * at the first line of the range. - */ - readonly range: Range; - - /** - * The ordered comments of the thread. - */ - comments: Comment[]; - - /** - * Whether the thread should be collapsed or expanded when opening the document. - * Defaults to Collapsed. - */ - collapsibleState: CommentThreadCollapsibleState; - - /** - * The optional human-readable label describing the [Comment Thread](#CommentThread) - */ - label?: string; - /** * Optional accept input command * @@ -1038,46 +992,6 @@ declare module 'vscode' { * This command will disabled when the comment editor is empty. */ acceptInputCommand?: Command; - - - /** - * Dispose this comment thread. - * - * Once disposed, this comment thread will be removed from visible editors and Comment Panel when approriate. - */ - dispose(): void; - } - - /** - * Author information of a [comment](#Comment) - */ - - export interface CommentAuthorInformation { - /** - * The display name of the author of the comment - */ - name: string; - - /** - * The optional icon path for the author - */ - iconPath?: Uri; - } - - /** - * Author information of a [comment](#Comment) - */ - - export interface CommentAuthorInformation { - /** - * The display name of the author of the comment - */ - name: string; - - /** - * The optional icon path for the author - */ - iconPath?: Uri; } /** @@ -1089,22 +1003,6 @@ declare module 'vscode' { */ id: string; - /** - * The human-readable comment body - */ - body: MarkdownString; - - /** - * The author information of the comment - */ - author: CommentAuthorInformation; - - /** - * Optional label describing the [Comment](#Comment) - * Label will be rendered next to authorName if exists. - */ - label?: string; - /** * The command to be executed if the comment is selected in the Comments Panel */ @@ -1124,16 +1022,6 @@ declare module 'vscode' { * Setter and getter for the contents of the comment input box */ value: string; - - /** - * The uri of the document comment input box has been created on - */ - resource: Uri; - - /** - * The range the comment input box is located within the document - */ - range: Range; } /** @@ -1146,36 +1034,12 @@ declare module 'vscode' { provideCommentingRanges(document: TextDocument, token: CancellationToken): ProviderResult; } - /** - * Comment thread template for new comment thread creation. - */ - export interface CommentThreadTemplate { + export interface EmptyCommentThreadFactory { /** - * The human-readable label describing the [Comment Thread](#CommentThread) + * The method `createEmptyCommentThread` is called when users attempt to create new comment thread from the gutter or command palette. + * Extensions still need to call `createCommentThread` inside this call when appropriate. */ - readonly label: string; - - /** - * Optional accept input command - * - * `acceptInputCommand` is the default action rendered on Comment Widget, which is always placed rightmost. - * This command will be invoked when users the user accepts the value in the comment editor. - * This command will disabled when the comment editor is empty. - */ - readonly acceptInputCommand?: Command; - - /** - * Optional additonal commands. - * - * `additionalCommands` are the secondary actions rendered on Comment Widget. - */ - readonly additionalCommands?: Command[]; - - /** - * The command to be executed when users try to delete the comment thread. Currently, this is only called - * when the user collapses a comment thread that has no comments in it. - */ - readonly deleteCommand?: Command; + createEmptyCommentThread(document: TextDocument, range: Range): ProviderResult; } /** @@ -1183,41 +1047,12 @@ declare module 'vscode' { * provide users various ways to interact with comments. */ export interface CommentController { - /** - * The id of this comment controller. - */ - readonly id: string; - - /** - * The human-readable label of this comment controller. - */ - readonly label: string; /** * The active [comment input box](#CommentInputBox) or `undefined`. The active `inputBox` is the input box of * the comment thread widget that currently has focus. It's `undefined` when the focus is not in any CommentInputBox. */ - readonly inputBox: CommentInputBox | undefined; - - /** - * Optional comment thread template information. - * - * The comment controller will use this information to create the comment widget when users attempt to create new comment thread - * from the gutter or command palette. - * - * When users run `CommentThreadTemplate.acceptInputCommand` or `CommentThreadTemplate.additionalCommands`, extensions should create - * the approriate [CommentThread](#CommentThread). - * - * If not provided, users won't be able to create new comment threads in the editor. - */ - template?: CommentThreadTemplate; - - /** - * Optional commenting range provider. Provide a list [ranges](#Range) which support commenting to any given resource uri. - * - * If not provided and `emptyCommentThreadFactory` exits, users can leave comments in any document opened in the editor. - */ - commentingRangeProvider?: CommentingRangeProvider; + readonly inputBox?: CommentInputBox; /** * Create a [comment thread](#CommentThread). The comment thread will be displayed in visible text editors (if the resource matches) @@ -1230,6 +1065,16 @@ declare module 'vscode' { */ createCommentThread(id: string, uri: Uri, range: Range, comments: Comment[]): CommentThread; + /** + * Optional new comment thread factory. + */ + emptyCommentThreadFactory?: EmptyCommentThreadFactory; + + /** + * Optional reaction provider + */ + reactionProvider?: CommentReactionProvider; + /** * Dispose this comment controller. * diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 4b4896e6ece..3371106e1a5 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -106,6 +106,16 @@ export class MainThreadCommentThread implements modes.CommentThread2 { this._onDidChangeLabel.fire(this._label); } + private _contextValue: string | undefined; + + get contextValue(): string | undefined { + return this._contextValue; + } + + set contextValue(context: string | undefined) { + this._contextValue = context; + } + private _onDidChangeLabel = new Emitter(); get onDidChangeLabel(): Event { return this._onDidChangeLabel.event; } @@ -204,6 +214,7 @@ export class MainThreadCommentThread implements modes.CommentThread2 { batchUpdate( range: IRange, label: string, + contextValue: string | undefined, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], @@ -211,6 +222,7 @@ export class MainThreadCommentThread implements modes.CommentThread2 { collapsibleState: modes.CommentThreadCollapsibleState) { this._range = range; this._label = label; + this._contextValue = contextValue; this._comments = comments; this._acceptInputCommand = acceptInputCommand; this._additionalCommands = additionalCommands; @@ -247,6 +259,10 @@ export class MainThreadCommentController { return this._id; } + get contextValue(): string { + return this._id; + } + get proxy(): ExtHostCommentsShape { return this._proxy; } @@ -319,13 +335,14 @@ export class MainThreadCommentController { resource: UriComponents, range: IRange, label: string, + contextValue: string | undefined, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], deleteCommand: modes.Command | undefined, collapsibleState: modes.CommentThreadCollapsibleState): void { let thread = this.getKnownThread(commentThreadHandle); - thread.batchUpdate(range, label, comments, acceptInputCommand, additionalCommands, deleteCommand, collapsibleState); + thread.batchUpdate(range, label, contextValue, comments, acceptInputCommand, additionalCommands, deleteCommand, collapsibleState); this._commentService.updateComments(this._uniqueId, { added: [], @@ -377,31 +394,26 @@ export class MainThreadCommentController { } let commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); + let staticContribution = await this._proxy.$checkStaticContribution(this.handle); return { owner: this._uniqueId, label: this.label, threads: ret, - commentingRanges: commentingRanges ? - { - resource: resource, ranges: commentingRanges, newCommentThreadCallback: async (uri: UriComponents, range: IRange) => { - let threadHandle = await this._proxy.$createNewCommentWidgetCallback(this.handle, uri, range, token); + commentingRanges: commentingRanges ? { + resource: resource, + ranges: commentingRanges, + newCommentThreadCallback: staticContribution ? undefined : async (uri: UriComponents, range: IRange) => { + let threadHandle = await this._proxy.$createNewCommentWidgetCallback(this.handle, uri, range, token); - if (threadHandle !== undefined) { - return this.getKnownThread(threadHandle); - } - - return; + if (threadHandle !== undefined) { + return this.getKnownThread(threadHandle); } - } : [], - draftMode: modes.DraftMode.NotSupported, - template: this._features.commentThreadTemplate ? { - controllerHandle: this.handle, - label: this._features.commentThreadTemplate.label, - acceptInputCommand: this._features.commentThreadTemplate.acceptInputCommand, - additionalCommands: this._features.commentThreadTemplate.additionalCommands, - deleteCommand: this._features.commentThreadTemplate.deleteCommand - } : undefined + + return; + } + } : [], + draftMode: modes.DraftMode.NotSupported }; } @@ -427,26 +439,8 @@ export class MainThreadCommentController { return ret; } - getCommentThreadFromTemplate(resource: UriComponents, range: IRange): MainThreadCommentThread { - let thread = new MainThreadCommentThread( - -1, - this.handle, - '', - '', - URI.revive(resource).toString(), - range - ); - - let template = this._features.commentThreadTemplate; - - if (template) { - thread.acceptInputCommand = template.acceptInputCommand; - thread.additionalCommands = template.additionalCommands; - thread.deleteCommand = template.deleteCommand; - thread.label = template.label; - } - - return thread; + createCommentThreadTemplate(resource: UriComponents, range: IRange): void { + this._proxy.$createCommentThreadTemplate(this.handle, resource, range); } toJSON(): any { @@ -467,8 +461,6 @@ export class MainThreadComments extends Disposable implements MainThreadComments private _handlers = new Map(); private _commentControllers = new Map(); - private _activeCommentThread?: MainThreadCommentThread; - private _input?: modes.CommentInput; private _openPanelListener: IDisposable | null; constructor( @@ -477,32 +469,12 @@ export class MainThreadComments extends Disposable implements MainThreadComments @ICommentService private readonly _commentService: ICommentService, @IPanelService private readonly _panelService: IPanelService, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IConfigurationService private readonly _configurationService: IConfigurationService + @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super(); this._disposables = []; this._activeCommentThreadDisposables = []; this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); - this._disposables.push(this._commentService.onDidChangeActiveCommentThread(async thread => { - let handle = (thread as MainThreadCommentThread).controllerHandle; - let controller = this._commentControllers.get(handle); - - if (!controller) { - return; - } - - this._activeCommentThreadDisposables = dispose(this._activeCommentThreadDisposables); - this._activeCommentThread = thread as MainThreadCommentThread; - controller.activeCommentThread = this._activeCommentThread; - - this._activeCommentThreadDisposables.push(this._activeCommentThread.onDidChangeInput(input => { // todo, dispose - this._input = input; - this._proxy.$onCommentWidgetInputChange(handle, URI.parse(this._activeCommentThread!.resource), this._activeCommentThread!.range, this._input ? this._input.value : undefined); - })); - - await this._proxy.$onActiveCommentThreadChange(controller.handle, controller.activeCommentThread.commentThreadHandle); - await this._proxy.$onCommentWidgetInputChange(controller.handle, URI.parse(this._activeCommentThread!.resource), this._activeCommentThread.range, this._input ? this._input.value : undefined); - })); } $registerCommentController(handle: number, id: string, label: string): void { @@ -562,6 +534,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments resource: UriComponents, range: IRange, label: string, + contextValue: string | undefined, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], @@ -573,7 +546,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments return undefined; } - return provider.updateCommentThread(commentThreadHandle, threadId, resource, range, label, comments, acceptInputCommand, additionalCommands, deleteCommand, collapsibleState); + return provider.updateCommentThread(commentThreadHandle, threadId, resource, range, label, contextValue, comments, acceptInputCommand, additionalCommands, deleteCommand, collapsibleState); } $deleteCommentThread(handle: number, commentThreadHandle: number) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9902a90c40c..e5a79e59dbb 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -138,7 +138,7 @@ export interface MainThreadCommentsShape extends IDisposable { $unregisterCommentController(handle: number): void; $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void; $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange): modes.CommentThread2 | undefined; - $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange, label: string, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], deleteCommand: modes.Command | undefined, collapseState: modes.CommentThreadCollapsibleState): void; + $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange, label: string, contextValue: string | undefined, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], deleteCommand: modes.Command | undefined, collapseState: modes.CommentThreadCollapsibleState): void; $deleteCommentThread(handle: number, commentThreadHandle: number): void; $setInputValue(handle: number, input: string): void; $registerDocumentCommentProvider(handle: number, features: CommentProviderFeatures): void; @@ -1208,9 +1208,10 @@ export interface ExtHostProgressShape { export interface ExtHostCommentsShape { $provideDocumentComments(handle: number, document: UriComponents): Promise; $createNewCommentThread(handle: number, document: UriComponents, range: IRange, text: string): Promise; + $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange): void; $onCommentWidgetInputChange(commentControllerHandle: number, document: UriComponents, range: IRange, input: string | undefined): Promise; - $onActiveCommentThreadChange(commentControllerHandle: number, threadHandle: number | undefined): Promise; $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise; + $checkStaticContribution(commentControllerHandle: number): 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): Promise; diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index a36f7409934..fb783e83775 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -69,6 +69,70 @@ export class ExtHostComments implements ExtHostCommentsShape { } return commentThread; + } else if (arg && arg.$mid === 8) { + const commentController = this._commentControllers.get(arg.thread.commentControlHandle); + + if (!commentController) { + return arg; + } + + const commentThread = commentController.getCommentThread(arg.thread.commentThreadHandle); + + if (!commentThread) { + return arg; + } + + return { + thread: commentThread, + text: arg.text + }; + } else if (arg && arg.$mid === 9) { + const commentController = this._commentControllers.get(arg.thread.commentControlHandle); + + if (!commentController) { + return arg; + } + + const commentThread = commentController.getCommentThread(arg.thread.commentThreadHandle); + + if (!commentThread) { + return arg; + } + + let commentUniqueId = arg.commentUniqueId; + + let comment = commentThread.getCommentByUniqueId(commentUniqueId); + + if (!comment) { + return arg; + } + + return comment; + + } else if (arg && arg.$mid === 10) { + const commentController = this._commentControllers.get(arg.thread.commentControlHandle); + + if (!commentController) { + return arg; + } + + const commentThread = commentController.getCommentThread(arg.thread.commentThreadHandle); + + if (!commentThread) { + return arg; + } + + let body = arg.text; + let commentUniqueId = arg.commentUniqueId; + + let comment = commentThread.getCommentByUniqueId(commentUniqueId); + + if (!comment) { + return arg; + } + + comment.body = body; + return comment; } return arg; @@ -88,6 +152,16 @@ export class ExtHostComments implements ExtHostCommentsShape { return commentController; } + $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange): void { + const commentController = this._commentControllers.get(commentControllerHandle); + + if (!commentController) { + return; + } + + commentController.$createCommentThreadTemplate(uriComponents, range); + } + $onCommentWidgetInputChange(commentControllerHandle: number, uriComponents: UriComponents, range: IRange, input: string): Promise { const commentController = this._commentControllers.get(commentControllerHandle); @@ -99,17 +173,6 @@ export class ExtHostComments implements ExtHostCommentsShape { return Promise.resolve(commentControllerHandle); } - $onActiveCommentThreadChange(commentControllerHandle: number, threadHandle: number): Promise { - const commentController = this._commentControllers.get(commentControllerHandle); - - if (!commentController) { - return Promise.resolve(undefined); - } - - commentController.$onActiveCommentThreadChange(threadHandle); - return Promise.resolve(threadHandle); - } - $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise { const commentController = this._commentControllers.get(commentControllerHandle); @@ -164,7 +227,7 @@ export class ExtHostComments implements ExtHostCommentsShape { return Promise.resolve(); } - if (!(commentController as any).emptyCommentThreadFactory && !(commentController.commentingRangeProvider && commentController.commentingRangeProvider.createEmptyCommentThread)) { + if (!(commentController as any).emptyCommentThreadFactory) { return Promise.resolve(); } @@ -173,13 +236,23 @@ export class ExtHostComments implements ExtHostCommentsShape { if ((commentController as any).emptyCommentThreadFactory) { return (commentController as any).emptyCommentThreadFactory!.createEmptyCommentThread(document, extHostTypeConverter.Range.to(range)); } - - if (commentController.commentingRangeProvider && commentController.commentingRangeProvider.createEmptyCommentThread) { - return commentController.commentingRangeProvider.createEmptyCommentThread(document, extHostTypeConverter.Range.to(range)); - } }).then(() => Promise.resolve()); } + $checkStaticContribution(commentControllerHandle: number): Promise { + const commentController = this._commentControllers.get(commentControllerHandle); + + if (!commentController) { + return Promise.resolve(false); + } + + if (!(commentController as any).emptyCommentThreadFactory) { + return Promise.resolve(true); + } + + return Promise.resolve(false); + } + registerWorkspaceCommentProvider( extensionId: ExtensionIdentifier, provider: vscode.WorkspaceCommentProvider @@ -376,12 +449,18 @@ export class ExtHostComments implements ExtHostCommentsShape { export class ExtHostCommentThread implements vscode.CommentThread { private static _handlePool: number = 0; readonly handle = ExtHostCommentThread._handlePool++; + public commentHandle: number = 0; + + set threadId(id: string) { + this._id = id; + } + get threadId(): string { - return this._id; + return this._id!; } get id(): string { - return this._id; + return this._id!; } get resource(): vscode.Uri { @@ -417,6 +496,17 @@ export class ExtHostCommentThread implements vscode.CommentThread { this._onDidUpdateCommentThread.fire(); } + private _contextValue: string | undefined; + + get contextValue(): string | undefined { + return this._contextValue; + } + + set contextValue(context: string | undefined) { + this._contextValue = context; + this._onDidUpdateCommentThread.fire(); + } + get comments(): vscode.Comment[] { return this._comments; } @@ -475,15 +565,21 @@ export class ExtHostCommentThread implements vscode.CommentThread { return this._isDiposed; } + private _commentsMap: Map = new Map(); + constructor( private _proxy: MainThreadCommentsShape, private readonly _commandsConverter: CommandsConverter, private _commentController: ExtHostCommentController, - private _id: string, + private _id: string | undefined, private _uri: vscode.Uri, private _range: vscode.Range, private _comments: vscode.Comment[] ) { + if (this._id === undefined) { + this._id = `${_commentController.id}.${this.handle}`; + } + this._proxy.$createCommentThread( this._commentController.handle, this.handle, @@ -507,7 +603,8 @@ export class ExtHostCommentThread implements vscode.CommentThread { eventuallyUpdateCommentThread(): void { const commentThreadRange = extHostTypeConverter.Range.from(this._range); const label = this.label; - const comments = this._comments.map(cmt => { return convertToModeComment(this._commentController, cmt, this._commandsConverter); }); + const contextValue = this.contextValue; + const comments = this._comments.map(cmt => { return convertToModeComment2(this, this._commentController, cmt, this._commandsConverter, this._commentsMap); }); const acceptInputCommand = this._acceptInputCommand ? this._commandsConverter.toInternal(this._acceptInputCommand) : undefined; const additionalCommands = this._additionalCommands ? this._additionalCommands.map(x => this._commandsConverter.toInternal(x)) : []; const deleteCommand = this._deleteCommand ? this._commandsConverter.toInternal(this._deleteCommand) : undefined; @@ -516,10 +613,11 @@ export class ExtHostCommentThread implements vscode.CommentThread { this._proxy.$updateCommentThread( this._commentController.handle, this.handle, - this._id, + this._id!, this._uri, commentThreadRange, label, + contextValue, comments, acceptInputCommand, additionalCommands, @@ -538,6 +636,18 @@ export class ExtHostCommentThread implements vscode.CommentThread { return undefined; } + getCommentByUniqueId(uniqueId: number): vscode.Comment | undefined { + for (let key of this._commentsMap) { + let comment = key[0]; + let id = key[1]; + if (uniqueId === id) { + return comment; + } + } + + return; + } + dispose() { this._localDisposables.forEach(disposable => disposable.dispose()); this._proxy.$deleteCommentThread( @@ -599,15 +709,6 @@ class ExtHostCommentController implements vscode.CommentController { } public inputBox: ExtHostCommentInputBox | undefined; - private _activeCommentThread: ExtHostCommentThread | undefined; - - public get activeCommentThread(): ExtHostCommentThread | undefined { - if (this._activeCommentThread && this._activeCommentThread.isDisposed) { - this._activeCommentThread = undefined; - } - - return this._activeCommentThread; - } public activeCommentingRange?: vscode.Range; @@ -618,30 +719,6 @@ class ExtHostCommentController implements vscode.CommentController { private _threads: Map = new Map(); commentingRangeProvider?: vscode.CommentingRangeProvider & { createEmptyCommentThread: (document: vscode.TextDocument, range: types.Range) => Promise; }; - private _template: vscode.CommentThreadTemplate | undefined; - - get template(): vscode.CommentThreadTemplate | undefined { - return this._template; - } - - set template(newTemplate: vscode.CommentThreadTemplate | undefined) { - this._template = newTemplate; - - if (newTemplate) { - const acceptInputCommand = newTemplate.acceptInputCommand ? this._commandsConverter.toInternal(newTemplate.acceptInputCommand) : undefined; - const additionalCommands = newTemplate.additionalCommands ? newTemplate.additionalCommands.map(x => this._commandsConverter.toInternal(x)) : []; - const deleteCommand = newTemplate.deleteCommand ? this._commandsConverter.toInternal(newTemplate.deleteCommand) : undefined; - this._proxy.$updateCommentControllerFeatures(this.handle, { - commentThreadTemplate: { - label: newTemplate.label, - acceptInputCommand, - additionalCommands, - deleteCommand - } - }); - } - } - private _commentReactionProvider?: vscode.CommentReactionProvider; get reactionProvider(): vscode.CommentReactionProvider | undefined { @@ -666,8 +743,23 @@ class ExtHostCommentController implements vscode.CommentController { this._proxy.$registerCommentController(this.handle, _id, _label); } - createCommentThread(id: string, resource: vscode.Uri, range: vscode.Range, comments: vscode.Comment[]): vscode.CommentThread { - const commentThread = new ExtHostCommentThread(this._proxy, this._commandsConverter, this, id, resource, range, comments); + createCommentThread(resource: vscode.Uri, range: vscode.Range, comments: vscode.Comment[]): vscode.CommentThread; + createCommentThread(id: string, resource: vscode.Uri, range: vscode.Range, comments: vscode.Comment[]): vscode.CommentThread; + createCommentThread(arg0: vscode.Uri | string, arg1: vscode.Uri | vscode.Range, arg2: vscode.Range | vscode.Comment[], arg3?: vscode.Comment[]): vscode.CommentThread { + if (typeof arg0 === 'string') { + const commentThread = new ExtHostCommentThread(this._proxy, this._commandsConverter, this, arg0, arg1 as vscode.Uri, arg2 as vscode.Range, arg3 as vscode.Comment[]); + this._threads.set(commentThread.handle, commentThread); + return commentThread; + } else { + const commentThread = new ExtHostCommentThread(this._proxy, this._commandsConverter, this, undefined, arg0 as vscode.Uri, arg1 as vscode.Range, arg2 as vscode.Comment[]); + this._threads.set(commentThread.handle, commentThread); + return commentThread; + } + } + + $createCommentThreadTemplate(uriComponents: UriComponents, range: IRange) { + const commentThread = new ExtHostCommentThread(this._proxy, this._commandsConverter, this, undefined, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), []); + commentThread.collapsibleState = modes.CommentThreadCollapsibleState.Expanded; this._threads.set(commentThread.handle, commentThread); return commentThread; } @@ -680,10 +772,6 @@ class ExtHostCommentController implements vscode.CommentController { } } - $onActiveCommentThreadChange(threadHandle: number) { - this._activeCommentThread = this.getCommentThread(threadHandle); - } - getCommentThread(handle: number) { return this._threads.get(handle); } @@ -759,16 +847,25 @@ function convertFromComment(comment: modes.Comment): vscode.Comment { count: reaction.count, hasReacted: reaction.hasReacted }; - }) : undefined + }) : undefined, + mode: comment.mode ? comment.mode : modes.CommentMode.Preview }; } -function convertToModeComment(commentController: ExtHostCommentController, vscodeComment: vscode.Comment, commandsConverter: CommandsConverter): modes.Comment { - const iconPath = vscodeComment.author && vscodeComment.author.iconPath ? vscodeComment.author.iconPath.toString() : - (vscodeComment.userIconPath ? vscodeComment.userIconPath.toString() : vscodeComment.gravatar); +function convertToModeComment2(thread: ExtHostCommentThread, commentController: ExtHostCommentController, vscodeComment: vscode.Comment, commandsConverter: CommandsConverter, commentsMap: Map): modes.Comment { + let commentUniqueId = commentsMap.get(vscodeComment)!; + if (!commentUniqueId) { + commentUniqueId = ++thread.commentHandle; + commentsMap.set(vscodeComment, commentUniqueId); + } + + const iconPath = vscodeComment.author && vscodeComment.author.iconPath ? vscodeComment.author.iconPath.toString() : undefined; return { commentId: vscodeComment.id || vscodeComment.commentId, + mode: vscodeComment.mode, + contextValue: vscodeComment.contextValue, + uniqueIdInThread: commentUniqueId, body: extHostTypeConverter.MarkdownString.from(vscodeComment.body), userName: vscodeComment.author ? vscodeComment.author.name : vscodeComment.userName, userIconPath: iconPath, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 143da3168a6..e9cc8187307 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2297,6 +2297,12 @@ export enum CommentThreadCollapsibleState { */ Expanded = 1 } + +export enum CommentMode { + Editing = 0, + Preview = 1 +} + //#endregion @es5ClassCompat diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index b56953901b7..fc4983b446a 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -46,6 +46,10 @@ namespace schema { case 'statusBar/windowIndicator': return MenuId.StatusBarWindowIndicatorMenu; case 'view/title': return MenuId.ViewTitle; case 'view/item/context': return MenuId.ViewItemContext; + case 'comments/commentThread/title': return MenuId.CommentThreadTitle; + case 'comments/commentThread/context': return MenuId.CommentThreadActions; + case 'comments/comment/title': return MenuId.CommentTitle; + case 'comments/comment/context': return MenuId.CommentActions; } return undefined; @@ -191,7 +195,27 @@ namespace schema { description: localize('view.itemContext', "The contributed view item context menu"), type: 'array', items: menuItem - } + }, + 'comments/commentThread/title': { + description: localize('commentThread.title', "The contributed comment thread title menu"), + type: 'array', + items: menuItem + }, + 'comments/commentThread/actions': { + description: localize('commentThread.actions', "The contributed comment thread actions"), + type: 'array', + items: menuItem + }, + 'comments/comment/title': { + description: localize('comment.title', "The contributed comment title menu"), + type: 'array', + items: menuItem + }, + 'comments/comment/actions': { + description: localize('comment.actions', "The contributed comment actions"), + type: 'array', + items: menuItem + }, } }; diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index a2cfe7b4bde..81438c3676b 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -673,6 +673,8 @@ export function createApiFactory( } }; + const comments = comment; + // namespace: debug const debug: typeof vscode.debug = { get activeDebugSession() { @@ -756,6 +758,7 @@ export function createApiFactory( languages, scm, comment, + comments, tasks, window, workspace, @@ -771,6 +774,7 @@ export function createApiFactory( ColorInformation: extHostTypes.ColorInformation, ColorPresentation: extHostTypes.ColorPresentation, CommentThreadCollapsibleState: extHostTypes.CommentThreadCollapsibleState, + CommentMode: extHostTypes.CommentMode, CompletionItem: extHostTypes.CompletionItem, CompletionItemKind: extHostTypes.CompletionItemKind, CompletionList: extHostTypes.CompletionList, diff --git a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts new file mode 100644 index 00000000000..72f669eda8a --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { IAction } from 'vs/base/common/actions'; +import { Disposable, dispose } from 'vs/base/common/lifecycle'; +import { IMenu } from 'vs/platform/actions/common/actions'; +import { attachButtonStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export class CommentFormActions extends Disposable { + private _buttonElements: HTMLElement[] = []; + + constructor( + private container: HTMLElement, + private actionHandler: (action: IAction) => void, + private themeService: IThemeService + ) { + super(); + } + + setActions(menu: IMenu) { + dispose(this._toDispose); + this._buttonElements.forEach(b => DOM.removeNode(b)); + + const groups = menu.getActions({ shouldForwardArgs: true }); + for (const group of groups) { + const [, actions] = group; + + actions.forEach(action => { + const button = new Button(this.container); + this._buttonElements.push(button.element); + + this._toDispose.push(button); + this._toDispose.push(attachButtonStyler(button, this.themeService)); + this._toDispose.push(button.onDidClick(() => this.actionHandler(action))); + + button.enabled = action.enabled; + button.label = action.label; + }); + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/comments/browser/commentMenus.ts b/src/vs/workbench/contrib/comments/browser/commentMenus.ts new file mode 100644 index 00000000000..3d83ab86f0c --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentMenus.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; +import { IAction } from 'vs/base/common/actions'; +import { MainThreadCommentController } from 'vs/workbench/api/browser/mainThreadComments'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { Comment, CommentThread2 } from 'vs/editor/common/modes'; +import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; + +export class CommentMenus implements IDisposable { + constructor( + controller: MainThreadCommentController, + @IContextKeyService private contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + @IContextMenuService private readonly contextMenuService: IContextMenuService + ) { + const commentControllerKey = this.contextKeyService.createKey('commentController', undefined); + + commentControllerKey.set(controller.contextValue); + } + + getCommentThreadTitleActions(commentThread: CommentThread2, contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.CommentThreadTitle, contextKeyService); + } + + getCommentThreadActions(commentThread: CommentThread2, contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.CommentThreadActions, contextKeyService); + } + + getCommentTitleActions(comment: Comment, contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.CommentTitle, contextKeyService); + } + + getCommentActions(comment: Comment, contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.CommentActions, contextKeyService); + } + + private getMenu(menuId: MenuId, contextKeyService: IContextKeyService): IMenu { + const menu = this.menuService.createMenu(menuId, contextKeyService); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + fillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => true); + + return menu; + } + + dispose(): void { + + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 8b4b7bbfe0a..8d9cfdc2585 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -8,7 +8,7 @@ import * as dom from 'vs/base/browser/dom'; import * as modes from 'vs/editor/common/modes'; import { ActionsOrientation, ActionViewItem, 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 { Action, IActionRunner, IAction } from 'vs/base/common/actions'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; @@ -35,6 +35,11 @@ import { ToggleReactionsAction, ReactionAction, ReactionActionViewItem } from '. import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; +import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; const UPDATE_COMMENT_LABEL = nls.localize('label.updateComment', "Update comment"); const UPDATE_IN_PROGRESS_LABEL = nls.localize('label.updatingComment', "Updating comment..."); @@ -57,10 +62,13 @@ export class CommentNode extends Disposable { private _updateCommentButton: Button; private _errorEditingContainer: HTMLElement; private _isPendingLabel: HTMLElement; + private _contextKeyService: IContextKeyService; + private _commentContextValue: IContextKey; private _deleteAction: Action; protected actionRunner?: IActionRunner; protected toolbar: ToolBar; + private _commentFormActions: CommentFormActions; private _onDidDelete = new Emitter(); @@ -85,12 +93,17 @@ export class CommentNode extends Disposable { @IModelService private modelService: IModelService, @IModeService private modeService: IModeService, @IDialogService private dialogService: IDialogService, + @IKeybindingService private keybindingService: IKeybindingService, @INotificationService private notificationService: INotificationService, - @IContextMenuService private contextMenuService: IContextMenuService + @IContextMenuService private contextMenuService: IContextMenuService, + @IContextKeyService contextKeyService: IContextKeyService ) { super(); this._domNode = dom.$('div.review-comment'); + this._contextKeyService = contextKeyService.createScoped(this._domNode); + this._commentContextValue = this._contextKeyService.createKey('comment', comment.contextValue); + this._domNode.tabIndex = 0; const avatar = dom.append(this._domNode, dom.$('div.avatar-container')); if (comment.userIconPath) { @@ -139,7 +152,7 @@ export class CommentNode extends Disposable { } private createActionsToolbar() { - const actions: Action[] = []; + const actions: IAction[] = []; let reactionGroup = this.commentService.getReactionGroup(this.owner); if (reactionGroup && reactionGroup.length) { @@ -163,6 +176,17 @@ export class CommentNode extends Disposable { actions.push(this._deleteAction); } + let commentMenus = this.commentService.getCommentMenus(this.owner); + const menu = commentMenus.getCommentTitleActions(this.comment, this._contextKeyService); + this._toDispose.push(menu); + this._toDispose.push(menu.onDidChange(e => { + const contributedActions = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], []); + this.toolbar.setActions(contributedActions); + })); + + const contributedActions = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], []); + actions.push(...contributedActions); + if (actions.length) { this.toolbar = new ToolBar(this._actionsToolbarContainer, this.contextMenuService, { actionViewItemProvider: action => { @@ -185,6 +209,12 @@ export class CommentNode extends Disposable { orientation: ActionsOrientation.HORIZONTAL }); + this.toolbar.context = { + thread: this.commentThread, + commentUniqueId: this.comment.uniqueIdInThread, + $mid: 9 + }; + this.registerActionBarListeners(this._actionsToolbarContainer); this.toolbar.setActions(actions, [])(); this._toDispose.push(this.toolbar); @@ -196,12 +226,15 @@ export class CommentNode extends Disposable { if (action.id === 'comment.delete' || action.id === 'comment.edit' || action.id === ToggleReactionsAction.ID) { options = { label: false, icon: true }; } else { - options = { label: true, icon: true }; + options = { label: false, icon: true }; } if (action.id === ReactionAction.ID) { let item = new ReactionActionViewItem(action); return item; + } else if (action instanceof MenuItemAction) { + let item = new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return item; } else { let item = new ActionViewItem({}, action, options); return item; @@ -391,14 +424,12 @@ export class CommentNode extends Disposable { uri: this._commentEditor.getModel()!.uri, value: this.comment.body.value }; - this.commentService.setActiveCommentThread(commentThread); this._commentEditorDisposables.push(this._commentEditor.onDidFocusEditorWidget(() => { commentThread.input = { uri: this._commentEditor!.getModel()!.uri, value: this.comment.body.value }; - this.commentService.setActiveCommentThread(commentThread); })); this._commentEditorDisposables.push(this._commentEditor.onDidChangeModelContent(e => { @@ -419,10 +450,15 @@ export class CommentNode extends Disposable { private removeCommentEditor() { this.isEditing = false; - this._editAction.enabled = true; + if (this._editAction) { + this._editAction.enabled = true; + } this._body.classList.remove('hidden'); - this._commentEditorModel.dispose(); + if (this._commentEditorModel) { + this._commentEditorModel.dispose(); + } + this._commentEditorDisposables.forEach(dispose => dispose.dispose()); this._commentEditorDisposables = []; if (this._commentEditor) { @@ -450,7 +486,6 @@ export class CommentNode extends Disposable { uri: this._commentEditor.getModel()!.uri, value: newBody }; - this.commentService.setActiveCommentThread(commentThread); let commandId = this.comment.editCommand.id; let args = this.comment.editCommand.arguments || []; @@ -488,7 +523,6 @@ export class CommentNode extends Disposable { if (result.confirmed) { try { if (this.comment.deleteCommand) { - this.commentService.setActiveCommentThread(this.commentThread as modes.CommentThread2); let commandId = this.comment.deleteCommand.id; let args = this.comment.deleteCommand.arguments || []; @@ -512,41 +546,81 @@ export class CommentNode extends Disposable { }); } + public switchToEditMode() { + if (this.isEditing) { + return; + } + + this.isEditing = true; + this._body.classList.add('hidden'); + this._commentEditContainer = dom.append(this._commentDetailsContainer, dom.$('.edit-container')); + this.createCommentEditor(); + this._errorEditingContainer = dom.append(this._commentEditContainer, dom.$('.validation-error.hidden')); + const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions')); + + const menus = this.commentService.getCommentMenus(this.owner); + const menu = menus.getCommentActions(this.comment, this._contextKeyService); + + this._toDispose.push(menu); + this._toDispose.push(menu.onDidChange(() => { + this._commentFormActions.setActions(menu); + })); + + this._commentFormActions = new CommentFormActions(formActions, (action: IAction): void => { + let text = this._commentEditor!.getValue(); + + action.run({ + thread: this.commentThread, + commentUniqueId: this.comment.uniqueIdInThread, + text: text, + $mid: 10 + }); + + this.removeCommentEditor(); + }, this.themeService); + + this._commentFormActions.setActions(menu); + } + private createEditAction(commentDetailsContainer: HTMLElement): Action { return new Action('comment.edit', nls.localize('label.edit', "Edit"), 'octicon octicon-pencil', true, () => { - this.isEditing = true; - this._body.classList.add('hidden'); - this._commentEditContainer = dom.append(commentDetailsContainer, dom.$('.edit-container')); - this.createCommentEditor(); - - this._errorEditingContainer = dom.append(this._commentEditContainer, dom.$('.validation-error.hidden')); - const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions')); - - const cancelEditButton = new Button(formActions); - cancelEditButton.label = nls.localize('label.cancel', "Cancel"); - this._toDispose.push(attachButtonStyler(cancelEditButton, this.themeService)); - - this._toDispose.push(cancelEditButton.onDidClick(_ => { - this.removeCommentEditor(); - })); - - this._updateCommentButton = new Button(formActions); - this._updateCommentButton.label = UPDATE_COMMENT_LABEL; - this._toDispose.push(attachButtonStyler(this._updateCommentButton, this.themeService)); - - this._toDispose.push(this._updateCommentButton.onDidClick(_ => { - this.editComment(); - })); - - this._commentEditorDisposables.push(this._commentEditor!.onDidChangeModelContent(_ => { - this._updateCommentButton.enabled = !!this._commentEditor!.getValue(); - })); - - this._editAction.enabled = false; - return Promise.resolve(); + return this.editCommentAction(commentDetailsContainer); }); } + private editCommentAction(commentDetailsContainer: HTMLElement) { + this.isEditing = true; + this._body.classList.add('hidden'); + this._commentEditContainer = dom.append(commentDetailsContainer, dom.$('.edit-container')); + this.createCommentEditor(); + + this._errorEditingContainer = dom.append(this._commentEditContainer, dom.$('.validation-error.hidden')); + const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions')); + + const cancelEditButton = new Button(formActions); + cancelEditButton.label = nls.localize('label.cancel', "Cancel"); + this._toDispose.push(attachButtonStyler(cancelEditButton, this.themeService)); + + this._toDispose.push(cancelEditButton.onDidClick(_ => { + this.removeCommentEditor(); + })); + + this._updateCommentButton = new Button(formActions); + this._updateCommentButton.label = UPDATE_COMMENT_LABEL; + this._toDispose.push(attachButtonStyler(this._updateCommentButton, this.themeService)); + + this._toDispose.push(this._updateCommentButton.onDidClick(_ => { + this.editComment(); + })); + + this._commentEditorDisposables.push(this._commentEditor!.onDidChangeModelContent(_ => { + this._updateCommentButton.enabled = !!this._commentEditor!.getValue(); + })); + + this._editAction.enabled = false; + return Promise.resolve(); + } + private registerActionBarListeners(actionsContainer: HTMLElement): void { this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseenter', () => { actionsContainer.classList.remove('hidden'); @@ -581,6 +655,14 @@ export class CommentNode extends Disposable { this._body.appendChild(this._md); } + if (newComment.mode !== undefined && newComment.mode !== this.comment.mode) { + if (newComment.mode === modes.CommentMode.Editing) { + this.switchToEditMode(); + } else { + this.removeCommentEditor(); + } + } + const shouldUpdateActions = newComment.editCommand !== this.comment.editCommand || newComment.deleteCommand !== this.comment.deleteCommand; this.comment = newComment; @@ -610,6 +692,12 @@ export class CommentNode extends Disposable { if (this.comment.commentReactions && this.comment.commentReactions.length) { this.createReactionsContainer(this._commentDetailsContainer); } + + if (this.comment.contextValue) { + this._commentContextValue.set(this.comment.contextValue); + } else { + this._commentContextValue.reset(); + } } focus() { diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index 296267ceb60..86ee098d6b2 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction, CommentingRanges, CommentThread2 } from 'vs/editor/common/modes'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -14,6 +14,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { assign } from 'vs/base/common/objects'; import { ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { MainThreadCommentController } from 'vs/workbench/api/browser/mainThreadComments'; +import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; export const ICommentService = createDecorator('commentService'); @@ -37,9 +38,7 @@ export interface ICommentService { readonly onDidSetResourceCommentInfos: Event; readonly onDidSetAllCommentThreads: Event; readonly onDidUpdateCommentThreads: Event; - readonly onDidChangeActiveCommentThread: Event; readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }>; - readonly onDidChangeInput: Event; readonly onDidSetDataProvider: Event; readonly onDidDeleteDataProvider: Event; setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void; @@ -47,6 +46,9 @@ export interface ICommentService { removeWorkspaceComments(owner: string): void; registerCommentController(owner: string, commentControl: MainThreadCommentController): void; unregisterCommentController(owner: string): void; + getCommentController(owner: string): MainThreadCommentController | undefined; + createCommentThreadTemplate(owner: string, resource: URI, range: Range): void; + getCommentMenus(owner: string): CommentMenus; registerDataProvider(owner: string, commentProvider: DocumentCommentProvider): void; unregisterDataProvider(owner: string): void; updateComments(ownerId: string, event: CommentThreadChangedEvent): void; @@ -66,9 +68,6 @@ export interface ICommentService { 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; - getCommentThreadFromTemplate(owner: string, resource: URI, range: IRange, ): CommentThread2 | undefined; - setActiveCommentThread(commentThread: CommentThread | null): void; - setInput(input: string): void; } export class CommentService extends Disposable implements ICommentService { @@ -89,11 +88,6 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidUpdateCommentThreads: Emitter = this._register(new Emitter()); readonly onDidUpdateCommentThreads: Event = this._onDidUpdateCommentThreads.event; - private readonly _onDidChangeActiveCommentThread = this._register(new Emitter()); - readonly onDidChangeActiveCommentThread: Event = this._onDidChangeActiveCommentThread.event; - - private readonly _onDidChangeInput: Emitter = this._register(new Emitter()); - readonly onDidChangeInput: Event = this._onDidChangeInput.event; private readonly _onDidChangeActiveCommentingRange: Emitter<{ range: Range, commentingRangesInfo: CommentingRanges @@ -106,19 +100,14 @@ export class CommentService extends Disposable implements ICommentService { private _commentProviders = new Map(); private _commentControls = new Map(); + private _commentMenus = new Map(); - constructor() { + constructor( + @IInstantiationService protected instantiationService: IInstantiationService + ) { super(); } - setActiveCommentThread(commentThread: CommentThread | null) { - this._onDidChangeActiveCommentThread.fire(commentThread); - } - - setInput(input: string) { - this._onDidChangeInput.fire(input); - } - setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void { this._onDidSetResourceCommentInfos.fire({ resource, commentInfos }); } @@ -141,6 +130,32 @@ export class CommentService extends Disposable implements ICommentService { this._onDidDeleteDataProvider.fire(owner); } + getCommentController(owner: string): MainThreadCommentController | undefined { + return this._commentControls.get(owner); + } + + createCommentThreadTemplate(owner: string, resource: URI, range: Range): void { + const commentController = this._commentControls.get(owner); + + if (!commentController) { + return; + } + + commentController.createCommentThreadTemplate(resource, range); + } + + getCommentMenus(owner: string): CommentMenus { + if (this._commentMenus.get(owner)) { + return this._commentMenus.get(owner)!; + } + + let controller = this._commentControls.get(owner); + + let menu = this.instantiationService.createInstance(CommentMenus, controller!); + this._commentMenus.set(owner, menu); + return menu; + } + registerDataProvider(owner: string, commentProvider: DocumentCommentProvider): void { this._commentProviders.set(owner, commentProvider); this._onDidSetDataProvider.fire(); @@ -256,16 +271,6 @@ export class CommentService extends Disposable implements ICommentService { } } - getCommentThreadFromTemplate(owner: string, resource: URI, range: IRange, ): CommentThread2 | undefined { - const commentController = this._commentControls.get(owner); - - if (commentController) { - return commentController.getCommentThreadFromTemplate(resource, range); - } - - return undefined; - } - getReactionGroup(owner: string): CommentReaction[] | undefined { const commentProvider = this._commentControls.get(owner); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index db53215e8e6..d9a4fb60eac 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -5,9 +5,9 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar, ActionViewItem } 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, IAction } from 'vs/base/common/actions'; import * as arrays from 'vs/base/common/arrays'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; @@ -38,9 +38,18 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { generateUuid } from 'vs/base/common/uuid'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; +import { MenuItemAction, IMenu } from 'vs/platform/actions/common/actions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; +import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; -const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-x'; +const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-chevron-up'; const COMMENT_SCHEME = 'comment'; @@ -70,6 +79,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private _styleElement: HTMLStyleElement; private _formActions: HTMLElement | null; private _error: HTMLElement; + private _contextKeyService: IContextKeyService; + private _threadIsEmpty: IContextKey; + private _commentThreadContextValue: IContextKey; + private _commentEditorIsEmpty: IContextKey; + private _commentFormActions: CommentFormActions; public get owner(): string { return this._owner; @@ -86,6 +100,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget return this._draftMode; } + private _commentMenus: CommentMenus; + constructor( editor: ICodeEditor, private _owner: string, @@ -98,15 +114,25 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget @IModelService private modelService: IModelService, @IThemeService private themeService: IThemeService, @ICommentService private commentService: ICommentService, - @IOpenerService private openerService: IOpenerService + @IOpenerService private openerService: IOpenerService, + @IKeybindingService private keybindingService: IKeybindingService, + @INotificationService private notificationService: INotificationService, + @IContextMenuService private contextMenuService: IContextMenuService, + @IContextKeyService contextKeyService: IContextKeyService ) { super(editor, { keepEditorSelection: true }); + this._contextKeyService = contextKeyService.createScoped(this.domNode); + this._threadIsEmpty = CommentContextKeys.commentThreadIsEmpty.bindTo(this._contextKeyService); + this._threadIsEmpty.set(!_commentThread.comments || !_commentThread.comments.length); + this._commentThreadContextValue = contextKeyService.createKey('commentThread', _commentThread.contextValue); + this._resizeObserver = null; this._isExpanded = _commentThread.collapsibleState ? _commentThread.collapsibleState === modes.CommentThreadCollapsibleState.Expanded : undefined; this._globalToDispose = []; this._commentThreadDisposables = []; this._submitActionsDisposables = []; this._formActions = null; + this._commentMenus = this.commentService.getCommentMenus(this._owner); this.create(); this._styleElement = dom.createStyleSheet(this.domNode); @@ -185,10 +211,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._bodyElement = dom.$('.body'); container.appendChild(this._bodyElement); - - dom.addDisposableListener(this._bodyElement, dom.EventType.FOCUS_IN, e => { - this.commentService.setActiveCommentThread(this._commentThread); - }); } protected _fillHead(container: HTMLElement): void { @@ -198,12 +220,41 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this.createThreadLabel(); const actionsContainer = dom.append(this._headElement, dom.$('.review-actions')); - this._actionbarWidget = new ActionBar(actionsContainer, {}); + this._actionbarWidget = new ActionBar(actionsContainer, { + actionViewItemProvider: (action: IAction) => { + if (action instanceof MenuItemAction) { + let item = new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return item; + } else { + let item = new ActionViewItem({}, action, { label: false, icon: true }); + return item; + } + } + }); + this._disposables.push(this._actionbarWidget); this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => this.collapse()); - this._actionbarWidget.push(this._collapseAction, { label: false, icon: true }); + if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { + const menu = this._commentMenus.getCommentThreadTitleActions(this._commentThread as modes.CommentThread2, this._contextKeyService); + this.setActionBarActions(menu); + + this._disposables.push(menu); + this._disposables.push(menu.onDidChange(e => { + this.setActionBarActions(menu); + })); + } else { + this._actionbarWidget.push([this._collapseAction], { label: false, icon: true }); + } + + this._actionbarWidget.context = this._commentThread; + } + + private setActionBarActions(menu: IMenu): void { + const groups = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], []); + this._actionbarWidget.clear(); + this._actionbarWidget.push([...groups, this._collapseAction], { label: false, icon: true }); } public collapse(): Promise { @@ -214,7 +265,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } else { const deleteCommand = (this._commentThread as modes.CommentThread2).deleteCommand; if (deleteCommand) { - this.commentService.setActiveCommentThread(this._commentThread); return this.commandService.executeCommand(deleteCommand.id, ...(deleteCommand.arguments || [])); } else if (this._commentEditor.getValue() === '') { this.dispose(); @@ -245,9 +295,10 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } - async update(commentThread: modes.CommentThread | modes.CommentThread2, replaceTemplate: boolean = false) { + async update(commentThread: modes.CommentThread | modes.CommentThread2) { const oldCommentsLen = this._commentElements.length; const newCommentsLen = commentThread.comments ? commentThread.comments.length : 0; + this._threadIsEmpty.set(!newCommentsLen); let commentElementsToDel: CommentNode[] = []; let commentElementsToDelIndex: number[] = []; @@ -294,26 +345,12 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThread = commentThread; this._commentElements = newCommentNodeList; - this.createThreadLabel(replaceTemplate); - - if (replaceTemplate) { - // since we are replacing the old comment thread, we need to rebind the listeners. - this._commentThreadDisposables.forEach(global => global.dispose()); - this._commentThreadDisposables = []; - } - - if (replaceTemplate) { - this.createTextModelListener(); - } + this.createThreadLabel(); if (this._formActions && this._commentEditor.hasModel()) { dom.clearNode(this._formActions); const model = this._commentEditor.getModel(); this.createCommentWidgetActions2(this._formActions, model); - - if (replaceTemplate) { - this.createCommentWidgetActionsListener(this._formActions, model); - } } // Move comment glyph widget and show position if the line has changed. @@ -346,6 +383,12 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this.hide(); } } + + if (this._commentThread.contextValue) { + this._commentThreadContextValue.set(this._commentThread.contextValue); + } else { + this._commentThreadContextValue.reset(); + } } updateDraftMode(draftMode: modes.DraftMode | undefined) { @@ -368,7 +411,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentEditor.layout({ height: 5 * 18, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ }); } - display(lineNumber: number, fromTemplate: boolean = false) { + display(lineNumber: number) { this._commentGlyph = new CommentGlyphWidget(this.editor, lineNumber); this._disposables.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); @@ -394,18 +437,30 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; this._commentForm = dom.append(this._bodyElement, dom.$('.comment-form')); this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, this._commentForm, SimpleCommentEditor.getEditorOptions(), this._parentEditor, this); + this._commentEditorIsEmpty = CommentContextKeys.commentIsEmpty.bindTo(this._contextKeyService); + this._commentEditorIsEmpty.set(!this._pendingComment); const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID); const params = JSON.stringify({ extensionId: this.extensionId, commentThreadId: this.commentThread.threadId }); - const resource = URI.parse(`${COMMENT_SCHEME}:commentinput-${modeId}.md?${params}`); + + let resource = URI.parse(`${COMMENT_SCHEME}://${this.extensionId}/commentinput-${modeId}.md?${params}`); // TODO. Remove params once extensions adopt authority. + let commentController = this.commentService.getCommentController(this.owner); + if (commentController) { + resource = resource.with({ authority: commentController.id }); + } + const model = this.modelService.createModel(this._pendingComment || '', this.modeService.createByFilepathOrFirstLine(resource.path), resource, false); this._disposables.push(model); this._commentEditor.setModel(model); this._disposables.push(this._commentEditor); - this._disposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => this.setCommentEditorDecorations())); + this._disposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => { + this.setCommentEditorDecorations(); + this._commentEditorIsEmpty.set(!this._commentEditor.getValue()); + })); + if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { this.createTextModelListener(); } @@ -426,9 +481,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._formActions = dom.append(this._commentForm, dom.$('.form-actions')); if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { this.createCommentWidgetActions2(this._formActions, model); - if (!fromTemplate) { - this.createCommentWidgetActionsListener(this._formActions, model); - } + this.createCommentWidgetActionsListener(this._formActions, model); } else { this.createCommentWidgetActions(this._formActions, model); } @@ -462,7 +515,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget uri: this._commentEditor.getModel()!.uri, value: this._commentEditor.getValue() }; - this.commentService.setActiveCommentThread(this._commentThread); })); this._commentThreadDisposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => { @@ -674,7 +726,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget uri: this._commentEditor.getModel()!.uri, value: this._commentEditor.getValue() }; - this.commentService.setActiveCommentThread(this._commentThread); await this.commandService.executeCommand(acceptInputCommand.id, ...(acceptInputCommand.arguments || [])); })); @@ -699,11 +750,29 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget uri: this._commentEditor.getModel()!.uri, value: this._commentEditor.getValue() }; - this.commentService.setActiveCommentThread(this._commentThread); await this.commandService.executeCommand(command.id, ...(command.arguments || [])); })); }); } + + const menu = this._commentMenus.getCommentThreadActions(commentThread, this._contextKeyService); + + this._disposables.push(menu); + this._disposables.push(menu.onDidChange(() => { + this._commentFormActions.setActions(menu); + })); + + this._commentFormActions = new CommentFormActions(container, (action: IAction) => { + action.run({ + thread: this._commentThread, + text: this._commentEditor.getValue(), + $mid: 8 + }); + + this.hideReplyArea(); + }, this.themeService); + + this._commentFormActions.setActions(menu); } private createNewCommentNode(comment: modes.Comment): CommentNode { @@ -751,7 +820,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget uri: this._commentEditor.getModel()!.uri, value: this._commentEditor.getValue() }; - this.commentService.setActiveCommentThread(this._commentThread); let commandId = commentThread.acceptInputCommand.id; let args = commentThread.acceptInputCommand.arguments || []; @@ -827,14 +895,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } - private createThreadLabel(replaceTemplate: boolean = false) { + private createThreadLabel() { let label: string | undefined; if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { label = (this._commentThread as modes.CommentThread2).label; } - if (label === undefined && !replaceTemplate) { - // if it's for replacing the comment thread template, the comment thread widget title can be undefined as extensions may set it later + if (label === undefined) { if (this._commentThread.comments && this._commentThread.comments.length) { const participantsList = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', '); label = nls.localize('commentThreadParticipants', "Participants: {0}", participantsList); @@ -847,7 +914,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._headingLabel.innerHTML = strings.escape(label); this._headingLabel.setAttribute('aria-label', label); } - } private expandReplyArea() { @@ -857,6 +923,17 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } + private hideReplyArea() { + this._commentEditor.setValue(''); + this._pendingComment = ''; + if (dom.hasClass(this._commentForm, 'expand')) { + dom.removeClass(this._commentForm, 'expand'); + } + this._commentEditor.getDomNode()!.style.outline = ''; + this._error.textContent = ''; + dom.addClass(this._error, 'hidden'); + } + private createReplyButton() { this._reviewThreadReplyButton = dom.append(this._commentForm, dom.$('button.review-thread-reply-button')); if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index 866bd19ca5a..e1888faedda 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -64,7 +64,7 @@ class CommentingRangeDecoration { return this._decorationId; } - constructor(private _editor: ICodeEditor, private _ownerId: string, private _extensionId: string | undefined, private _label: string | undefined, private _range: IRange, private _reply: modes.Command | undefined, commentingOptions: ModelDecorationOptions, private _template: modes.CommentThreadTemplate | undefined, private commentingRangesInfo?: modes.CommentingRanges) { + constructor(private _editor: ICodeEditor, private _ownerId: string, private _extensionId: string | undefined, private _label: string | undefined, private _range: IRange, private _reply: modes.Command | undefined, commentingOptions: ModelDecorationOptions, private commentingRangesInfo?: modes.CommentingRanges) { const startLineNumber = _range.startLineNumber; const endLineNumber = _range.endLineNumber; let commentingRangeDecorations = [{ @@ -81,14 +81,13 @@ class CommentingRangeDecoration { } } - public getCommentAction(): { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined, template: modes.CommentThreadTemplate | undefined } { + public getCommentAction(): { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined } { return { extensionId: this._extensionId, label: this._label, replyCommand: this._reply, ownerId: this._ownerId, - commentingRangesInfo: this.commentingRangesInfo, - template: this._template + commentingRangesInfo: this.commentingRangesInfo }; } @@ -125,11 +124,11 @@ class CommentingRangeDecorator { for (const info of commentInfos) { if (Array.isArray(info.commentingRanges)) { info.commentingRanges.forEach(range => { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, info.reply, this.decorationOptions, info.template)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, info.reply, this.decorationOptions)); }); } else { (info.commentingRanges ? info.commentingRanges.ranges : []).forEach(range => { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, undefined, this.decorationOptions, info.template, info.commentingRanges as modes.CommentingRanges)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, undefined, this.decorationOptions, info.commentingRanges as modes.CommentingRanges)); }); } } @@ -424,7 +423,7 @@ export class ReviewController implements IEditorContribution { } removed.forEach(thread => { - let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== ''); if (matchedZones.length) { let matchedZone = matchedZones[0]; let index = this._commentWidgets.indexOf(matchedZone); @@ -449,7 +448,7 @@ export class ReviewController implements IEditorContribution { let matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && (zoneWidget.commentThread as any).commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); if (matchedNewCommentThreadZones.length) { - matchedNewCommentThreadZones[0].update(thread, true); + matchedNewCommentThreadZones[0].update(thread); return; } @@ -469,22 +468,6 @@ export class ReviewController implements IEditorContribution { this._commentWidgets.push(zoneWidget); } - private addCommentThreadFromTemplate(lineNumber: number, ownerId: string): ReviewZoneWidget { - let templateCommentThread = this.commentService.getCommentThreadFromTemplate(ownerId, this.editor.getModel()!.uri, { - startLineNumber: lineNumber, - startColumn: 1, - endLineNumber: lineNumber, - endColumn: 1 - })!; - - templateCommentThread.collapsibleState = modes.CommentThreadCollapsibleState.Expanded; - templateCommentThread.comments = []; - - let templateReviewZoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, ownerId, templateCommentThread, '', modes.DraftMode.NotSupported); - - return templateReviewZoneWidget; - } - private addComment(lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, draftMode: modes.DraftMode | undefined, pendingComment: string | null) { if (this._newCommentWidget) { this.notificationService.warn(`Please submit the comment at line ${this._newCommentWidget.position ? this._newCommentWidget.position.lineNumber : -1} before creating a new one.`); @@ -640,16 +623,16 @@ export class ReviewController implements IEditorContribution { const commentInfos = newCommentInfos.filter(info => info.ownerId === pick.id); if (commentInfos.length) { - const { replyCommand, ownerId, extensionId, commentingRangesInfo, template } = commentInfos[0]; - this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo, template); + const { replyCommand, ownerId, extensionId, commentingRangesInfo } = commentInfos[0]; + this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo); } }).then(() => { this._addInProgress = false; }); } } else { - const { replyCommand, ownerId, extensionId, commentingRangesInfo, template } = newCommentInfos[0]!; - this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo, template); + const { replyCommand, ownerId, extensionId, commentingRangesInfo } = newCommentInfos[0]!; + this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo); } return Promise.resolve(); @@ -668,11 +651,11 @@ export class ReviewController implements IEditorContribution { return picks; } - private getContextMenuActions(commentInfos: { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined, template: modes.CommentThreadTemplate | undefined }[], lineNumber: number): (IAction | ContextSubMenu)[] { + private getContextMenuActions(commentInfos: { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined }[], lineNumber: number): (IAction | ContextSubMenu)[] { const actions: (IAction | ContextSubMenu)[] = []; commentInfos.forEach(commentInfo => { - const { replyCommand, ownerId, extensionId, label, commentingRangesInfo, template } = commentInfo; + const { replyCommand, ownerId, extensionId, label, commentingRangesInfo } = commentInfo; actions.push(new Action( 'addCommentThread', @@ -680,7 +663,7 @@ export class ReviewController implements IEditorContribution { undefined, true, () => { - this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo, template); + this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo); return Promise.resolve(); } )); @@ -688,23 +671,10 @@ export class ReviewController implements IEditorContribution { return actions; } - public addCommentAtLine2(lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined, template: modes.CommentThreadTemplate | undefined) { + public addCommentAtLine2(lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined) { if (commentingRangesInfo) { let range = new Range(lineNumber, 1, lineNumber, 1); - if (template) { - // create comment widget through template - let commentThreadWidget = this.addCommentThreadFromTemplate(lineNumber, ownerId); - commentThreadWidget.display(lineNumber, true); - this._commentWidgets.push(commentThreadWidget); - commentThreadWidget.onDidClose(() => { - this._commentWidgets = this._commentWidgets.filter(zoneWidget => !( - zoneWidget.owner === commentThreadWidget.owner && - (zoneWidget.commentThread as any).commentThreadHandle === -1 && - Range.equalsRange(zoneWidget.commentThread.range, commentThreadWidget.commentThread.range) - )); - }); - this.processNextThreadToAdd(); - } else if (commentingRangesInfo.newCommentThreadCallback) { + if (commentingRangesInfo.newCommentThreadCallback) { return commentingRangesInfo.newCommentThreadCallback(this.editor.getModel()!.uri, range) .then(_ => { this.processNextThreadToAdd(); @@ -713,6 +683,11 @@ export class ReviewController implements IEditorContribution { this.notificationService.error(nls.localize('commentThreadAddFailure', "Adding a new comment thread failed: {0}.", e.message)); this.processNextThreadToAdd(); }); + } else { + // latest api, no comments creation callback + this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range); + this.processNextThreadToAdd(); + return; } } else { const commentInfo = this._commentInfos.filter(info => info.owner === ownerId); diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index aa195a0cd7a..4ed2c4be005 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -198,6 +198,16 @@ background-image: url(./reaction-hc.svg); } +.monaco-editor .review-widget .body .review-comment .comment-title .action-label { + display: block; + height: 18px; + line-height: 18px; + min-width: 28px; + background-size: 16px; + background-position: center center; + background-repeat: no-repeat; +} + .monaco-editor .review-widget .body .review-comment .comment-title .action-label.toolbar-toggle-pickReactions { background-image: url(./reaction.svg); width: 18px; diff --git a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts index f158e3f3294..12a7897fec6 100644 --- a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts +++ b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts @@ -22,6 +22,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; +import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; export const ctxCommentEditorFocused = new RawContextKey('commentEditorFocused', false); @@ -30,6 +31,7 @@ export class SimpleCommentEditor extends CodeEditorWidget { private _parentEditor: ICodeEditor; private _parentThread: ICommentThreadWidget; private _commentEditorFocused: IContextKey; + private _commentEditorEmpty: IContextKey; constructor( domElement: HTMLElement, @@ -56,11 +58,15 @@ export class SimpleCommentEditor extends CodeEditorWidget { super(domElement, options, codeEditorWidgetOptions, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService); - this._commentEditorFocused = ctxCommentEditorFocused.bindTo(this._contextKeyService); + this._commentEditorFocused = ctxCommentEditorFocused.bindTo(contextKeyService); + this._commentEditorEmpty = CommentContextKeys.commentIsEmpty.bindTo(contextKeyService); + this._commentEditorEmpty.set(!this.getValue()); this._parentEditor = parentEditor; this._parentThread = parentThread; this._register(this.onDidFocusEditorWidget(_ => this._commentEditorFocused.set(true))); + + this._register(this.onDidChangeModelContent(e => this._commentEditorEmpty.set(!this.getValue()))); this._register(this.onDidBlurEditorWidget(_ => this._commentEditorFocused.reset())); } diff --git a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts new file mode 100644 index 00000000000..3593481d5ca --- /dev/null +++ b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export namespace CommentContextKeys { + /** + * A context key that is set when the comment thread has no comments. + */ + export const commentThreadIsEmpty = new RawContextKey('commentThreadIsEmpty', false); + /** + * A context key that is set when the comment has no input. + */ + export const commentIsEmpty = new RawContextKey('commentIsEmpty', false); +} \ No newline at end of file