From e2da9fe481346c4733077ab32d770bc7df2220a7 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 7 Mar 2024 12:19:16 +0100 Subject: [PATCH] Add context menu to comments in comments panel to un/resolve conversation Part of #206898 --- src/vs/editor/common/languages.ts | 2 +- src/vs/monaco.d.ts | 2 +- src/vs/platform/actions/common/actions.ts | 1 + .../api/browser/mainThreadComments.ts | 12 +- .../comments/browser/commentService.ts | 138 ++++++++-------- .../browser/commentThreadZoneWidget.ts | 14 +- .../comments/browser/commentsController.ts | 88 +++++----- .../contrib/comments/browser/commentsModel.ts | 30 ++-- .../comments/browser/commentsTreeViewer.ts | 150 ++++++++++++++++-- .../contrib/comments/browser/commentsView.ts | 32 ---- .../contrib/comments/browser/media/panel.css | 36 +++++ .../contrib/comments/common/commentModel.ts | 39 +++-- .../test/browser/commentsView.test.ts | 1 + .../browser/view/cellParts/cellComments.ts | 2 +- .../actions/common/menusExtensionPoint.ts | 6 + .../common/extensionsApiProposals.ts | 1 + ...oposed.contribCommentsViewThreadMenus.d.ts | 6 + 17 files changed, 353 insertions(+), 207 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 4d157bf57884d..5af870d82ff3a 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1883,7 +1883,7 @@ export interface PendingCommentThread { body: string; range: IRange | undefined; uri: URI; - owner: string; + uniqueOwner: string; isReply: boolean; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 3391b58aa2e96..7553f3a26234e 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7838,7 +7838,7 @@ declare namespace monaco.languages { body: string; range: IRange | undefined; uri: Uri; - owner: string; + uniqueOwner: string; isReply: boolean; } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 98cd2259f98f8..b14c7d0b5361a 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -162,6 +162,7 @@ export class MenuId { static readonly CommentThreadCommentContext = new MenuId('CommentThreadCommentContext'); static readonly CommentTitle = new MenuId('CommentTitle'); static readonly CommentActions = new MenuId('CommentActions'); + static readonly CommentsViewThreadActions = new MenuId('CommentsViewThreadActions'); static readonly InteractiveToolbar = new MenuId('InteractiveToolbar'); static readonly InteractiveCellTitle = new MenuId('InteractiveCellTitle'); static readonly InteractiveCellDelete = new MenuId('InteractiveCellDelete'); diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 4e369eb19da41..8896e0b6e1c85 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -248,6 +248,10 @@ export class MainThreadCommentController implements ICommentController { return this._features; } + get owner() { + return this._id; + } + constructor( private readonly _proxy: ExtHostCommentsShape, private readonly _commentService: ICommentService, @@ -385,7 +389,7 @@ export class MainThreadCommentController implements ICommentController { async getDocumentComments(resource: URI, token: CancellationToken) { if (resource.scheme === Schemas.vscodeNotebookCell) { return { - owner: this._uniqueId, + uniqueOwner: this._uniqueId, label: this.label, threads: [], commentingRanges: { @@ -407,7 +411,7 @@ export class MainThreadCommentController implements ICommentController { const commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); return { - owner: this._uniqueId, + uniqueOwner: this._uniqueId, label: this.label, threads: ret, commentingRanges: { @@ -421,7 +425,7 @@ export class MainThreadCommentController implements ICommentController { async getNotebookComments(resource: URI, token: CancellationToken) { if (resource.scheme !== Schemas.vscodeNotebookCell) { return { - owner: this._uniqueId, + uniqueOwner: this._uniqueId, label: this.label, threads: [] }; @@ -436,7 +440,7 @@ export class MainThreadCommentController implements ICommentController { } return { - owner: this._uniqueId, + uniqueOwner: this._uniqueId, label: this.label, threads: ret }; diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index da2530446351a..accc000bdce45 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -31,14 +31,14 @@ interface IResourceCommentThreadEvent { } export interface ICommentInfo extends CommentInfo { - owner: string; + uniqueOwner: string; label?: string; } export interface INotebookCommentInfo { extensionId?: string; threads: CommentThread[]; - owner: string; + uniqueOwner: string; label?: string; } @@ -49,7 +49,7 @@ export interface IWorkspaceCommentThreadsEvent { } export interface INotebookCommentThreadChangedEvent extends CommentThreadChangedEvent { - owner: string; + uniqueOwner: string; } export interface ICommentController { @@ -62,6 +62,7 @@ export interface ICommentController { }; options?: CommentOptions; contextValue?: string; + owner: string; createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined): Promise; updateCommentThreadTemplate(threadHandle: number, range: IRange): Promise; deleteCommentThreadMain(commentThreadId: string): void; @@ -83,7 +84,7 @@ export interface ICommentService { readonly onDidUpdateNotebookCommentThreads: Event; readonly onDidChangeActiveEditingCommentThread: Event; readonly onDidChangeCurrentCommentThread: Event; - readonly onDidUpdateCommentingRanges: Event<{ owner: string }>; + readonly onDidUpdateCommentingRanges: Event<{ uniqueOwner: string }>; readonly onDidChangeActiveCommentingRange: Event<{ range: Range; commentingRangesInfo: CommentingRanges }>; readonly onDidSetDataProvider: Event; readonly onDidDeleteDataProvider: Event; @@ -91,28 +92,28 @@ export interface ICommentService { readonly isCommentingEnabled: boolean; readonly commentsModel: ICommentsModel; setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void; - setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void; - removeWorkspaceComments(owner: string): void; - registerCommentController(owner: string, commentControl: ICommentController): void; - unregisterCommentController(owner?: string): void; - getCommentController(owner: string): ICommentController | undefined; - createCommentThreadTemplate(owner: string, resource: URI, range: Range | undefined): Promise; - updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range): Promise; - getCommentMenus(owner: string): CommentMenus; + setWorkspaceComments(uniqueOwner: string, commentsByResource: CommentThread[]): void; + removeWorkspaceComments(uniqueOwner: string): void; + registerCommentController(uniqueOwner: string, commentControl: ICommentController): void; + unregisterCommentController(uniqueOwner?: string): void; + getCommentController(uniqueOwner: string): ICommentController | undefined; + createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined): Promise; + updateCommentThreadTemplate(uniqueOwner: string, threadHandle: number, range: Range): Promise; + getCommentMenus(uniqueOwner: string): CommentMenus; updateComments(ownerId: string, event: CommentThreadChangedEvent): void; updateNotebookComments(ownerId: string, event: CommentThreadChangedEvent): void; disposeCommentThread(ownerId: string, threadId: string): void; getDocumentComments(resource: URI): Promise<(ICommentInfo | null)[]>; getNotebookComments(resource: URI): Promise<(INotebookCommentInfo | null)[]>; updateCommentingRanges(ownerId: string, resourceHints?: CommentingRangeResourceHint): void; - hasReactionHandler(owner: string): boolean; - toggleReaction(owner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise; + hasReactionHandler(uniqueOwner: string): boolean; + toggleReaction(uniqueOwner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise; setActiveEditingCommentThread(commentThread: CommentThread | null): void; setCurrentCommentThread(commentThread: CommentThread | undefined): void; - setActiveCommentAndThread(owner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined): Promise; + setActiveCommentAndThread(uniqueOwner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined): Promise; enableCommenting(enable: boolean): void; registerContinueOnCommentProvider(provider: IContinueOnCommentProvider): IDisposable; - removeContinueOnComment(pendingComment: { range: IRange | undefined; uri: URI; owner: string; isReply?: boolean }): PendingCommentThread | undefined; + removeContinueOnComment(pendingComment: { range: IRange | undefined; uri: URI; uniqueOwner: string; isReply?: boolean }): PendingCommentThread | undefined; resourceHasCommentingRanges(resource: URI): boolean; } @@ -139,8 +140,8 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidUpdateNotebookCommentThreads: Emitter = this._register(new Emitter()); readonly onDidUpdateNotebookCommentThreads: Event = this._onDidUpdateNotebookCommentThreads.event; - private readonly _onDidUpdateCommentingRanges: Emitter<{ owner: string }> = this._register(new Emitter<{ owner: string }>()); - readonly onDidUpdateCommentingRanges: Event<{ owner: string }> = this._onDidUpdateCommentingRanges.event; + private readonly _onDidUpdateCommentingRanges: Emitter<{ uniqueOwner: string }> = this._register(new Emitter<{ uniqueOwner: string }>()); + readonly onDidUpdateCommentingRanges: Event<{ uniqueOwner: string }> = this._onDidUpdateCommentingRanges.event; private readonly _onDidChangeActiveEditingCommentThread = this._register(new Emitter()); readonly onDidChangeActiveEditingCommentThread = this._onDidChangeActiveEditingCommentThread.event; @@ -165,7 +166,7 @@ export class CommentService extends Disposable implements ICommentService { private _isCommentingEnabled: boolean = true; private _workspaceHasCommenting: IContextKey; - private _continueOnComments = new Map(); // owner -> PendingCommentThread[] + private _continueOnComments = new Map(); // uniqueOwner -> PendingCommentThread[] private _continueOnCommentProviders = new Set(); private readonly _commentsModel: CommentsModel = this._register(new CommentsModel()); @@ -200,15 +201,16 @@ export class CommentService extends Disposable implements ICommentService { } this.logService.debug(`Comments: URIs of continue on comments from storage ${commentsToRestore.map(thread => thread.uri.toString()).join(', ')}.`); const changedOwners = this._addContinueOnComments(commentsToRestore, this._continueOnComments); - for (const owner of changedOwners) { - const control = this._commentControls.get(owner); + for (const uniqueOwner of changedOwners) { + const control = this._commentControls.get(uniqueOwner); if (!control) { continue; } const evt: ICommentThreadChangedEvent = { - owner, + uniqueOwner: uniqueOwner, + owner: control.owner, ownerLabel: control.label, - pending: this._continueOnComments.get(owner) || [], + pending: this._continueOnComments.get(uniqueOwner) || [], added: [], removed: [], changed: [] @@ -294,8 +296,8 @@ export class CommentService extends Disposable implements ICommentService { } private _lastActiveCommentController: ICommentController | undefined; - async setActiveCommentAndThread(owner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined) { - const commentController = this._commentControls.get(owner); + async setActiveCommentAndThread(uniqueOwner: string, commentInfo: { thread: CommentThread; comment?: Comment } | undefined) { + const commentController = this._commentControls.get(uniqueOwner); if (!commentController) { return; @@ -312,8 +314,8 @@ export class CommentService extends Disposable implements ICommentService { this._onDidSetResourceCommentInfos.fire({ resource, commentInfos }); } - private setModelThreads(ownerId: string, ownerLabel: string, commentThreads: CommentThread[]) { - this._commentsModel.setCommentThreads(ownerId, ownerLabel, commentThreads); + private setModelThreads(ownerId: string, owner: string, ownerLabel: string, commentThreads: CommentThread[]) { + this._commentsModel.setCommentThreads(ownerId, owner, ownerLabel, commentThreads); this._onDidSetAllCommentThreads.fire({ ownerId, ownerLabel, commentThreads }); } @@ -322,45 +324,45 @@ export class CommentService extends Disposable implements ICommentService { this._onDidUpdateCommentThreads.fire(event); } - setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void { + setWorkspaceComments(uniqueOwner: string, commentsByResource: CommentThread[]): void { if (commentsByResource.length) { this._workspaceHasCommenting.set(true); } - const control = this._commentControls.get(owner); + const control = this._commentControls.get(uniqueOwner); if (control) { - this.setModelThreads(owner, control.label, commentsByResource); + this.setModelThreads(uniqueOwner, control.owner, control.label, commentsByResource); } } - removeWorkspaceComments(owner: string): void { - const control = this._commentControls.get(owner); + removeWorkspaceComments(uniqueOwner: string): void { + const control = this._commentControls.get(uniqueOwner); if (control) { - this.setModelThreads(owner, control.label, []); + this.setModelThreads(uniqueOwner, control.owner, control.label, []); } } - registerCommentController(owner: string, commentControl: ICommentController): void { - this._commentControls.set(owner, commentControl); + registerCommentController(uniqueOwner: string, commentControl: ICommentController): void { + this._commentControls.set(uniqueOwner, commentControl); this._onDidSetDataProvider.fire(); } - unregisterCommentController(owner?: string): void { - if (owner) { - this._commentControls.delete(owner); + unregisterCommentController(uniqueOwner?: string): void { + if (uniqueOwner) { + this._commentControls.delete(uniqueOwner); } else { this._commentControls.clear(); } - this._commentsModel.deleteCommentsByOwner(owner); - this._onDidDeleteDataProvider.fire(owner); + this._commentsModel.deleteCommentsByOwner(uniqueOwner); + this._onDidDeleteDataProvider.fire(uniqueOwner); } - getCommentController(owner: string): ICommentController | undefined { - return this._commentControls.get(owner); + getCommentController(uniqueOwner: string): ICommentController | undefined { + return this._commentControls.get(uniqueOwner); } - async createCommentThreadTemplate(owner: string, resource: URI, range: Range | undefined): Promise { - const commentController = this._commentControls.get(owner); + async createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined): Promise { + const commentController = this._commentControls.get(uniqueOwner); if (!commentController) { return; @@ -369,8 +371,8 @@ export class CommentService extends Disposable implements ICommentService { return commentController.createCommentThreadTemplate(resource, range); } - async updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range) { - const commentController = this._commentControls.get(owner); + async updateCommentThreadTemplate(uniqueOwner: string, threadHandle: number, range: Range) { + const commentController = this._commentControls.get(uniqueOwner); if (!commentController) { return; @@ -379,31 +381,31 @@ export class CommentService extends Disposable implements ICommentService { await commentController.updateCommentThreadTemplate(threadHandle, range); } - disposeCommentThread(owner: string, threadId: string) { - const controller = this.getCommentController(owner); + disposeCommentThread(uniqueOwner: string, threadId: string) { + const controller = this.getCommentController(uniqueOwner); controller?.deleteCommentThreadMain(threadId); } - getCommentMenus(owner: string): CommentMenus { - if (this._commentMenus.get(owner)) { - return this._commentMenus.get(owner)!; + getCommentMenus(uniqueOwner: string): CommentMenus { + if (this._commentMenus.get(uniqueOwner)) { + return this._commentMenus.get(uniqueOwner)!; } const menu = this.instantiationService.createInstance(CommentMenus); - this._commentMenus.set(owner, menu); + this._commentMenus.set(uniqueOwner, menu); return menu; } updateComments(ownerId: string, event: CommentThreadChangedEvent): void { const control = this._commentControls.get(ownerId); if (control) { - const evt: ICommentThreadChangedEvent = Object.assign({}, event, { owner: ownerId, ownerLabel: control.label }); + const evt: ICommentThreadChangedEvent = Object.assign({}, event, { uniqueOwner: ownerId, ownerLabel: control.label, owner: control.owner }); this.updateModelThreads(evt); } } updateNotebookComments(ownerId: string, event: CommentThreadChangedEvent): void { - const evt: INotebookCommentThreadChangedEvent = Object.assign({}, event, { owner: ownerId }); + const evt: INotebookCommentThreadChangedEvent = Object.assign({}, event, { uniqueOwner: ownerId }); this._onDidUpdateNotebookCommentThreads.fire(evt); } @@ -414,11 +416,11 @@ export class CommentService extends Disposable implements ICommentService { } } this._workspaceHasCommenting.set(true); - this._onDidUpdateCommentingRanges.fire({ owner: ownerId }); + this._onDidUpdateCommentingRanges.fire({ uniqueOwner: ownerId }); } - async toggleReaction(owner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise { - const commentController = this._commentControls.get(owner); + async toggleReaction(uniqueOwner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise { + const commentController = this._commentControls.get(uniqueOwner); if (commentController) { return commentController.toggleReaction(resource, thread, comment, reaction, CancellationToken.None); @@ -427,8 +429,8 @@ export class CommentService extends Disposable implements ICommentService { } } - hasReactionHandler(owner: string): boolean { - const commentProvider = this._commentControls.get(owner); + hasReactionHandler(uniqueOwner: string): boolean { + const commentProvider = this._commentControls.get(uniqueOwner); if (commentProvider) { return !!commentProvider.features.reactionHandler; @@ -447,10 +449,10 @@ export class CommentService extends Disposable implements ICommentService { // This can happen because continue on comments are stored separately from local un-submitted comments. for (const documentCommentThread of documentComments.threads) { if (documentCommentThread.comments?.length === 0 && documentCommentThread.range) { - this.removeContinueOnComment({ range: documentCommentThread.range, uri: resource, owner: documentComments.owner }); + this.removeContinueOnComment({ range: documentCommentThread.range, uri: resource, uniqueOwner: documentComments.uniqueOwner }); } } - const pendingComments = this._continueOnComments.get(documentComments.owner); + const pendingComments = this._continueOnComments.get(documentComments.uniqueOwner); documentComments.pendingCommentThreads = pendingComments?.filter(pendingComment => pendingComment.uri.toString() === resource.toString()); return documentComments; }) @@ -495,8 +497,8 @@ export class CommentService extends Disposable implements ICommentService { this.storageService.store(CONTINUE_ON_COMMENTS, commentsToSave, StorageScope.WORKSPACE, StorageTarget.USER); } - removeContinueOnComment(pendingComment: { range: IRange; uri: URI; owner: string; isReply?: boolean }): PendingCommentThread | undefined { - const pendingComments = this._continueOnComments.get(pendingComment.owner); + removeContinueOnComment(pendingComment: { range: IRange; uri: URI; uniqueOwner: string; isReply?: boolean }): PendingCommentThread | undefined { + const pendingComments = this._continueOnComments.get(pendingComment.uniqueOwner); if (pendingComments) { const commentIndex = pendingComments.findIndex(comment => comment.uri.toString() === pendingComment.uri.toString() && Range.equalsRange(comment.range, pendingComment.range) && (pendingComment.isReply === undefined || comment.isReply === pendingComment.isReply)); if (commentIndex > -1) { @@ -509,14 +511,14 @@ export class CommentService extends Disposable implements ICommentService { private _addContinueOnComments(pendingComments: PendingCommentThread[], map: Map): Set { const changedOwners = new Set(); for (const pendingComment of pendingComments) { - if (!map.has(pendingComment.owner)) { - map.set(pendingComment.owner, [pendingComment]); - changedOwners.add(pendingComment.owner); + if (!map.has(pendingComment.uniqueOwner)) { + map.set(pendingComment.uniqueOwner, [pendingComment]); + changedOwners.add(pendingComment.uniqueOwner); } else { - const commentsForOwner = map.get(pendingComment.owner)!; + const commentsForOwner = map.get(pendingComment.uniqueOwner)!; if (commentsForOwner.every(comment => (comment.uri.toString() !== pendingComment.uri.toString()) || !Range.equalsRange(comment.range, pendingComment.range))) { commentsForOwner.push(pendingComment); - changedOwners.add(pendingComment.owner); + changedOwners.add(pendingComment.uniqueOwner); } } } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index e5ae9040d5096..baa3438345dea 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -105,8 +105,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private _contextKeyService: IContextKeyService; private _scopedInstantiationService: IInstantiationService; - public get owner(): string { - return this._owner; + public get uniqueOwner(): string { + return this._uniqueOwner; } public get commentThread(): languages.CommentThread { return this._commentThread; @@ -120,7 +120,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget constructor( editor: ICodeEditor, - private _owner: string, + private _uniqueOwner: string, private _commentThread: languages.CommentThread, private _pendingComment: string | undefined, private _pendingEdits: { [key: number]: string } | undefined, @@ -137,7 +137,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget [IContextKeyService, this._contextKeyService] )); - const controller = this.commentService.getCommentController(this._owner); + const controller = this.commentService.getCommentController(this._uniqueOwner); if (controller) { this._commentOptions = controller.options; } @@ -229,7 +229,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget CommentThreadWidget, container, this.editor, - this._owner, + this._uniqueOwner, this.editor.getModel()!.uri, this._contextKeyService, this._scopedInstantiationService, @@ -258,7 +258,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } else { range = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.endColumn); } - await this.commentService.updateCommentThreadTemplate(this.owner, this._commentThread.commentThreadHandle, range); + await this.commentService.updateCommentThreadTemplate(this.uniqueOwner, this._commentThread.commentThreadHandle, range); } } }, @@ -281,7 +281,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private deleteCommentThread(): void { this.dispose(); - this.commentService.disposeCommentThread(this.owner, this._commentThread.threadId); + this.commentService.disposeCommentThread(this.uniqueOwner, this._commentThread.threadId); } public collapse() { diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 35c0cd6eb7f56..e83cb560c6c64 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -203,10 +203,10 @@ class CommentingRangeDecorator { intersectingEmphasisRange = new Range(intersectingSelectionRange.endLineNumber, 1, intersectingSelectionRange.endLineNumber, 1); intersectingSelectionRange = new Range(intersectingSelectionRange.startLineNumber, 1, intersectingSelectionRange.endLineNumber - 1, 1); } - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, intersectingSelectionRange, this.multilineDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, intersectingSelectionRange, this.multilineDecorationOptions, info.commentingRanges, true)); if (!this._lineHasThread(editor, intersectingEmphasisRange)) { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, intersectingEmphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, intersectingEmphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); } const beforeRangeEndLine = Math.min(intersectingEmphasisRange.startLineNumber, intersectingSelectionRange.startLineNumber) - 1; @@ -215,27 +215,27 @@ class CommentingRangeDecorator { const hasAfterRange = rangeObject.endLineNumber >= afterRangeStartLine; if (hasBeforeRange) { const beforeRange = new Range(range.startLineNumber, 1, beforeRangeEndLine, 1); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); } if (hasAfterRange) { const afterRange = new Range(afterRangeStartLine, 1, range.endLineNumber, 1); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); } } else if ((rangeObject.startLineNumber <= emphasisLine) && (emphasisLine <= rangeObject.endLineNumber)) { if (rangeObject.startLineNumber < emphasisLine) { const beforeRange = new Range(range.startLineNumber, 1, emphasisLine - 1, 1); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); } const emphasisRange = new Range(emphasisLine, 1, emphasisLine, 1); if (!this._lineHasThread(editor, emphasisRange)) { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, emphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, emphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); } if (emphasisLine < rangeObject.endLineNumber) { const afterRange = new Range(emphasisLine + 1, 1, range.endLineNumber, 1); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); } } else { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, this.decorationOptions, info.commentingRanges)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, range, this.decorationOptions, info.commentingRanges)); } }); } @@ -274,7 +274,7 @@ class CommentingRangeDecorator { return foundInfos.map(foundInfo => { return { action: { - ownerId: foundInfo.owner, + ownerId: foundInfo.uniqueOwner, extensionId: foundInfo.extensionId, label: foundInfo.label, commentingRangesInfo: foundInfo.commentingRanges @@ -290,7 +290,7 @@ class CommentingRangeDecorator { for (const decoration of this.commentingRangeDecorations) { const range = decoration.getActiveRange(); if (range && this.areRangesIntersectingOrTouchingByLine(range, commentRange)) { - // We can have several commenting ranges that match from the same owner because of how + // We can have several commenting ranges that match from the same uniqueOwner because of how // the line hover and selection decoration is done. // The ranges must be merged so that we can see if the new commentRange fits within them. const action = decoration.getCommentAction(); @@ -383,7 +383,7 @@ export class CommentController implements IEditorContribution { private _computeCommentingRangePromise!: CancelablePromise | null; private _computeCommentingRangeScheduler!: Delayer> | null; private _pendingNewCommentCache: { [key: string]: { [key: string]: string } }; - private _pendingEditsCache: { [key: string]: { [key: string]: { [key: number]: string } } }; // owner -> threadId -> uniqueIdInThread -> pending comment + private _pendingEditsCache: { [key: string]: { [key: string]: { [key: number]: string } } }; // uniqueOwner -> threadId -> uniqueIdInThread -> pending comment private _inProcessContinueOnComments: Map = new Map(); private _editorDisposables: IDisposable[] = []; private _activeCursorHasCommentingRange: IContextKey; @@ -496,7 +496,7 @@ export class CommentController implements IEditorContribution { if (pendingNewComment !== lastCommentBody) { pendingComments.push({ - owner: zone.owner, + uniqueOwner: zone.uniqueOwner, uri: zone.editor.getModel()!.uri, range: zone.commentThread.range, body: pendingNewComment, @@ -824,7 +824,7 @@ export class CommentController implements IEditorContribution { await this._computePromise; } - const commentInfo = this._commentInfos.filter(info => info.owner === e.owner); + const commentInfo = this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner); if (!commentInfo || !commentInfo.length) { return; } @@ -835,14 +835,14 @@ export class CommentController implements IEditorContribution { const pending = e.pending.filter(pending => pending.uri.toString() === editorURI.toString()); removed.forEach(thread => { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== ''); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== ''); if (matchedZones.length) { const matchedZone = matchedZones[0]; const index = this._commentWidgets.indexOf(matchedZone); this._commentWidgets.splice(index, 1); matchedZone.dispose(); } - const infosThreads = this._commentInfos.filter(info => info.owner === e.owner)[0].threads; + const infosThreads = this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner)[0].threads; for (let i = 0; i < infosThreads.length; i++) { if (infosThreads[i] === thread) { infosThreads.splice(i, 1); @@ -852,7 +852,7 @@ export class CommentController implements IEditorContribution { }); changed.forEach(thread => { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); if (matchedZones.length) { const matchedZone = matchedZones[0]; matchedZone.update(thread); @@ -860,19 +860,19 @@ export class CommentController implements IEditorContribution { } }); for (const thread of added) { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); if (matchedZones.length) { return; } - const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); + const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); if (matchedNewCommentThreadZones.length) { matchedNewCommentThreadZones[0].update(thread); return; } - const continueOnCommentIndex = this._inProcessContinueOnComments.get(e.owner)?.findIndex(pending => { + const continueOnCommentIndex = this._inProcessContinueOnComments.get(e.uniqueOwner)?.findIndex(pending => { if (pending.range === undefined) { return thread.range === undefined; } else { @@ -881,14 +881,14 @@ export class CommentController implements IEditorContribution { }); let continueOnCommentText: string | undefined; if ((continueOnCommentIndex !== undefined) && continueOnCommentIndex >= 0) { - continueOnCommentText = this._inProcessContinueOnComments.get(e.owner)?.splice(continueOnCommentIndex, 1)[0].body; + continueOnCommentText = this._inProcessContinueOnComments.get(e.uniqueOwner)?.splice(continueOnCommentIndex, 1)[0].body; } - const pendingCommentText = (this._pendingNewCommentCache[e.owner] && this._pendingNewCommentCache[e.owner][thread.threadId]) + const pendingCommentText = (this._pendingNewCommentCache[e.uniqueOwner] && this._pendingNewCommentCache[e.uniqueOwner][thread.threadId]) ?? continueOnCommentText; - const pendingEdits = this._pendingEditsCache[e.owner] && this._pendingEditsCache[e.owner][thread.threadId]; - this.displayCommentThread(e.owner, thread, pendingCommentText, pendingEdits); - this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); + const pendingEdits = this._pendingEditsCache[e.uniqueOwner] && this._pendingEditsCache[e.uniqueOwner][thread.threadId]; + this.displayCommentThread(e.uniqueOwner, thread, pendingCommentText, pendingEdits); + this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner)[0].threads.push(thread); this.tryUpdateReservedSpace(); } @@ -902,12 +902,12 @@ export class CommentController implements IEditorContribution { } private async resumePendingComment(editorURI: URI, thread: languages.PendingCommentThread) { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === thread.owner && Range.lift(zoneWidget.commentThread.range)?.equalsRange(thread.range)); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === thread.uniqueOwner && Range.lift(zoneWidget.commentThread.range)?.equalsRange(thread.range)); if (thread.isReply && matchedZones.length) { - this.commentService.removeContinueOnComment({ owner: thread.owner, uri: editorURI, range: thread.range, isReply: true }); + this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: true }); matchedZones[0].setPendingComment(thread.body); } else if (matchedZones.length) { - this.commentService.removeContinueOnComment({ owner: thread.owner, uri: editorURI, range: thread.range, isReply: false }); + this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: false }); const existingPendingComment = matchedZones[0].getPendingComments().newComment; // We need to try to reconcile the existing pending comment with the incoming pending comment let pendingComment: string; @@ -920,15 +920,15 @@ export class CommentController implements IEditorContribution { } matchedZones[0].setPendingComment(pendingComment); } else if (!thread.isReply) { - const threadStillAvailable = this.commentService.removeContinueOnComment({ owner: thread.owner, uri: editorURI, range: thread.range, isReply: false }); + const threadStillAvailable = this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: false }); if (!threadStillAvailable) { return; } - if (!this._inProcessContinueOnComments.has(thread.owner)) { - this._inProcessContinueOnComments.set(thread.owner, []); + if (!this._inProcessContinueOnComments.has(thread.uniqueOwner)) { + this._inProcessContinueOnComments.set(thread.uniqueOwner, []); } - this._inProcessContinueOnComments.get(thread.owner)?.push(thread); - await this.commentService.createCommentThreadTemplate(thread.owner, thread.uri, thread.range ? Range.lift(thread.range) : undefined); + this._inProcessContinueOnComments.get(thread.uniqueOwner)?.push(thread); + await this.commentService.createCommentThreadTemplate(thread.uniqueOwner, thread.uri, thread.range ? Range.lift(thread.range) : undefined); } } @@ -968,7 +968,7 @@ export class CommentController implements IEditorContribution { return undefined; } - private displayCommentThread(owner: string, thread: languages.CommentThread, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): void { + private displayCommentThread(uniqueOwner: string, thread: languages.CommentThread, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): void { const editor = this.editor?.getModel(); if (!editor) { return; @@ -979,9 +979,9 @@ export class CommentController implements IEditorContribution { let continueOnCommentReply: languages.PendingCommentThread | undefined; if (thread.range && !pendingComment) { - continueOnCommentReply = this.commentService.removeContinueOnComment({ owner, uri: editor.uri, range: thread.range, isReply: true }); + continueOnCommentReply = this.commentService.removeContinueOnComment({ uniqueOwner, uri: editor.uri, range: thread.range, isReply: true }); } - const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, owner, thread, pendingComment ?? continueOnCommentReply?.body, pendingEdits); + const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, uniqueOwner, thread, pendingComment ?? continueOnCommentReply?.body, pendingEdits); zoneWidget.display(thread.range); this._commentWidgets.push(zoneWidget); this.openCommentsView(thread); @@ -1259,8 +1259,8 @@ export class CommentController implements IEditorContribution { hasCommentingRanges = true; } - const providerCacheStore = this._pendingNewCommentCache[info.owner]; - const providerEditsCacheStore = this._pendingEditsCache[info.owner]; + const providerCacheStore = this._pendingNewCommentCache[info.uniqueOwner]; + const providerEditsCacheStore = this._pendingEditsCache[info.uniqueOwner]; info.threads = info.threads.filter(thread => !thread.isDisposed); info.threads.forEach(thread => { let pendingComment: string | undefined = undefined; @@ -1273,7 +1273,7 @@ export class CommentController implements IEditorContribution { pendingEdits = providerEditsCacheStore[thread.threadId]; } - this.displayCommentThread(info.owner, thread, pendingComment, pendingEdits); + this.displayCommentThread(info.uniqueOwner, thread, pendingComment, pendingEdits); }); for (const thread of info.pendingCommentThreads ?? []) { this.resumePendingComment(this.editor!.getModel()!.uri, thread); @@ -1303,7 +1303,7 @@ export class CommentController implements IEditorContribution { this._commentWidgets.forEach(zone => { const pendingComments = zone.getPendingComments(); const pendingNewComment = pendingComments.newComment; - const providerNewCommentCacheStore = this._pendingNewCommentCache[zone.owner]; + const providerNewCommentCacheStore = this._pendingNewCommentCache[zone.uniqueOwner]; let lastCommentBody; if (zone.commentThread.comments && zone.commentThread.comments.length) { @@ -1316,10 +1316,10 @@ export class CommentController implements IEditorContribution { } if (pendingNewComment && (pendingNewComment !== lastCommentBody)) { if (!providerNewCommentCacheStore) { - this._pendingNewCommentCache[zone.owner] = {}; + this._pendingNewCommentCache[zone.uniqueOwner] = {}; } - this._pendingNewCommentCache[zone.owner][zone.commentThread.threadId] = pendingNewComment; + this._pendingNewCommentCache[zone.uniqueOwner][zone.commentThread.threadId] = pendingNewComment; } else { if (providerNewCommentCacheStore) { delete providerNewCommentCacheStore[zone.commentThread.threadId]; @@ -1327,12 +1327,12 @@ export class CommentController implements IEditorContribution { } const pendingEdits = pendingComments.edits; - const providerEditsCacheStore = this._pendingEditsCache[zone.owner]; + const providerEditsCacheStore = this._pendingEditsCache[zone.uniqueOwner]; if (Object.keys(pendingEdits).length > 0) { if (!providerEditsCacheStore) { - this._pendingEditsCache[zone.owner] = {}; + this._pendingEditsCache[zone.uniqueOwner] = {}; } - this._pendingEditsCache[zone.owner][zone.commentThread.threadId] = pendingEdits; + this._pendingEditsCache[zone.uniqueOwner][zone.commentThread.threadId] = pendingEdits; } else if (providerEditsCacheStore) { delete providerEditsCacheStore[zone.commentThread.threadId]; } diff --git a/src/vs/workbench/contrib/comments/browser/commentsModel.ts b/src/vs/workbench/contrib/comments/browser/commentsModel.ts index 6d345350e83db..d0701d5f344b7 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsModel.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsModel.ts @@ -43,15 +43,15 @@ export class CommentsModel extends Disposable implements ICommentsModel { }); } - public setCommentThreads(owner: string, ownerLabel: string, commentThreads: CommentThread[]): void { - this.commentThreadsMap.set(owner, { ownerLabel, resourceWithCommentThreads: this.groupByResource(owner, commentThreads) }); + public setCommentThreads(uniqueOwner: string, owner: string, ownerLabel: string, commentThreads: CommentThread[]): void { + this.commentThreadsMap.set(uniqueOwner, { ownerLabel, resourceWithCommentThreads: this.groupByResource(uniqueOwner, owner, commentThreads) }); this.updateResourceCommentThreads(); } - public deleteCommentsByOwner(owner?: string): void { - if (owner) { - const existingOwner = this.commentThreadsMap.get(owner); - this.commentThreadsMap.set(owner, { ownerLabel: existingOwner?.ownerLabel, resourceWithCommentThreads: [] }); + public deleteCommentsByOwner(uniqueOwner?: string): void { + if (uniqueOwner) { + const existingOwner = this.commentThreadsMap.get(uniqueOwner); + this.commentThreadsMap.set(uniqueOwner, { ownerLabel: existingOwner?.ownerLabel, resourceWithCommentThreads: [] }); } else { this.commentThreadsMap.clear(); } @@ -59,9 +59,9 @@ export class CommentsModel extends Disposable implements ICommentsModel { } public updateCommentThreads(event: ICommentThreadChangedEvent): boolean { - const { owner, ownerLabel, removed, changed, added } = event; + const { uniqueOwner, owner, ownerLabel, removed, changed, added } = event; - const threadsForOwner = this.commentThreadsMap.get(owner)?.resourceWithCommentThreads || []; + const threadsForOwner = this.commentThreadsMap.get(uniqueOwner)?.resourceWithCommentThreads || []; removed.forEach(thread => { // Find resource that has the comment thread @@ -91,9 +91,9 @@ export class CommentsModel extends Disposable implements ICommentsModel { // Find comment node on resource that is that thread and replace it const index = matchingResourceData.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId); if (index >= 0) { - matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread); + matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, URI.parse(matchingResourceData.id), thread); } else if (thread.comments && thread.comments.length) { - matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread)); + matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, URI.parse(matchingResourceData.id), thread)); } }); @@ -102,14 +102,14 @@ export class CommentsModel extends Disposable implements ICommentsModel { if (existingResource.length) { const resource = existingResource[0]; if (thread.comments && thread.comments.length) { - resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, resource.resource, thread)); + resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, resource.resource, thread)); } } else { - threadsForOwner.push(new ResourceWithCommentThreads(owner, URI.parse(thread.resource!), [thread])); + threadsForOwner.push(new ResourceWithCommentThreads(uniqueOwner, owner, URI.parse(thread.resource!), [thread])); } }); - this.commentThreadsMap.set(owner, { ownerLabel, resourceWithCommentThreads: threadsForOwner }); + this.commentThreadsMap.set(uniqueOwner, { ownerLabel, resourceWithCommentThreads: threadsForOwner }); this.updateResourceCommentThreads(); return removed.length > 0 || changed.length > 0 || added.length > 0; @@ -127,11 +127,11 @@ export class CommentsModel extends Disposable implements ICommentsModel { } } - private groupByResource(owner: string, commentThreads: CommentThread[]): ResourceWithCommentThreads[] { + private groupByResource(uniqueOwner: string, owner: string, commentThreads: CommentThread[]): ResourceWithCommentThreads[] { const resourceCommentThreads: ResourceWithCommentThreads[] = []; const commentThreadsByResource = new Map(); for (const group of groupBy(commentThreads, CommentsModel._compareURIs)) { - commentThreadsByResource.set(group[0].resource!, new ResourceWithCommentThreads(owner, URI.parse(group[0].resource!), group)); + commentThreadsByResource.set(group[0].resource!, new ResourceWithCommentThreads(uniqueOwner, owner, URI.parse(group[0].resource!), group)); } commentThreadsByResource.forEach((v, i, m) => { diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 451bc2107893b..df131acad4bb4 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -10,7 +10,7 @@ import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { CommentNode, ResourceWithCommentThreads } from 'vs/workbench/contrib/comments/common/commentModel'; -import { ITreeFilter, ITreeNode, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; +import { ITreeContextMenuEvent, ITreeFilter, ITreeNode, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -34,6 +34,14 @@ import { ILocalizedString } from 'vs/platform/action/common/action'; import { CommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel'; import { setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; +import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IAction } from 'vs/base/common/actions'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export const COMMENTS_VIEW_ID = 'workbench.panel.comments'; export const COMMENTS_VIEW_STORAGE_ID = 'Comments'; @@ -62,6 +70,7 @@ interface ICommentThreadTemplateData { separator: HTMLElement; timestamp: TimestampWidget; }; + actionBar: ActionBar; disposables: IDisposable[]; } @@ -124,10 +133,59 @@ export class ResourceWithCommentsRenderer implements IListRenderer, ICommentThreadTemplateData> { templateId: string = 'comment-node'; constructor( + private actionViewItemProvider: IActionViewItemProvider, + private menus: CommentsMenus, @IOpenerService private readonly openerService: IOpenerService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeService private themeService: IThemeService @@ -137,16 +195,22 @@ export class CommentNodeRenderer implements IListRenderer const threadContainer = dom.append(container, dom.$('.comment-thread-container')); const metadataContainer = dom.append(threadContainer, dom.$('.comment-metadata-container')); + const metadata = dom.append(metadataContainer, dom.$('.comment-metadata')); const threadMetadata = { - icon: dom.append(metadataContainer, dom.$('.icon')), - userNames: dom.append(metadataContainer, dom.$('.user')), - timestamp: new TimestampWidget(this.configurationService, dom.append(metadataContainer, dom.$('.timestamp-container'))), - separator: dom.append(metadataContainer, dom.$('.separator')), - commentPreview: dom.append(metadataContainer, dom.$('.text')), - range: dom.append(metadataContainer, dom.$('.range')) + icon: dom.append(metadata, dom.$('.icon')), + userNames: dom.append(metadata, dom.$('.user')), + timestamp: new TimestampWidget(this.configurationService, dom.append(metadata, dom.$('.timestamp-container'))), + separator: dom.append(metadata, dom.$('.separator')), + commentPreview: dom.append(metadata, dom.$('.text')), + range: dom.append(metadata, dom.$('.range')) }; threadMetadata.separator.innerText = '\u00b7'; + const actionsContainer = dom.append(metadataContainer, dom.$('.actions')); + const actionBar = new ActionBar(actionsContainer, { + actionViewItemProvider: this.actionViewItemProvider + }); + const snippetContainer = dom.append(threadContainer, dom.$('.comment-snippet-container')); const repliesMetadata = { container: snippetContainer, @@ -158,9 +222,9 @@ export class CommentNodeRenderer implements IListRenderer }; repliesMetadata.separator.innerText = '\u00b7'; repliesMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.indent)); - const disposables = [threadMetadata.timestamp, repliesMetadata.timestamp]; - return { threadMetadata, repliesMetadata, disposables }; + const disposables = [threadMetadata.timestamp, repliesMetadata.timestamp]; + return { threadMetadata, repliesMetadata, actionBar, disposables }; } private getCountString(commentCount: number): string { @@ -198,6 +262,8 @@ export class CommentNodeRenderer implements IListRenderer } renderElement(node: ITreeNode, index: number, templateData: ICommentThreadTemplateData, height: number | undefined): void { + templateData.actionBar.clear(); + const commentCount = node.element.replies.length + 1; templateData.threadMetadata.icon.classList.remove(...Array.from(templateData.threadMetadata.icon.classList.values()) .filter(value => value.startsWith('codicon'))); @@ -232,6 +298,14 @@ export class CommentNodeRenderer implements IListRenderer } } + const menuActions = this.menus.getResourceActions(node.element); + templateData.actionBar.push(menuActions.actions, { icon: true, label: false }); + templateData.actionBar.context = { + commentControlHandle: node.element.controllerHandle, + commentThreadHandle: node.element.threadHandle, + $mid: MarshalledId.CommentThread + }; + if (!node.element.hasReply()) { templateData.repliesMetadata.container.style.display = 'none'; return; @@ -250,6 +324,7 @@ export class CommentNodeRenderer implements IListRenderer disposeTemplate(templateData: ICommentThreadTemplateData): void { templateData.disposables.forEach(disposeable => disposeable.dispose()); + templateData.actionBar.dispose(); } } @@ -347,6 +422,8 @@ export class Filter implements ITreeFilter { + private readonly menus: CommentsMenus; + constructor( labels: ResourceLabels, container: HTMLElement, @@ -355,12 +432,16 @@ export class CommentsList extends WorkbenchObjectTree this.commentsOnContextMenu(e))); + } + + private commentsOnContextMenu(treeEvent: ITreeContextMenuEvent): void { + const node: CommentsModel | ResourceWithCommentThreads | CommentNode | null = treeEvent.element; + if (!(node instanceof CommentNode)) { + return; + } + const event: UIEvent = treeEvent.browserEvent; + + event.preventDefault(); + event.stopPropagation(); + + this.setFocus([node]); + const actions = this.menus.getResourceContextActions(node); + if (!actions.length) { + return; + } + this.contextMenuService.showContextMenu({ + getAnchor: () => treeEvent.anchor, + getActions: () => actions, + getActionViewItem: (action) => { + const keybinding = this.keybindingService.lookupKeybinding(action.id); + if (keybinding) { + return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); + } + return undefined; + }, + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this.domFocus(); + } + }, + getActionsContext: () => ({ + commentControlHandle: node.controllerHandle, + commentThreadHandle: node.threadHandle, + $mid: MarshalledId.CommentThread + }) + }); } filterComments(): void { diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 385ac16e1dc88..2530cf96e7c03 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -13,7 +13,6 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CommentNode, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { IWorkspaceCommentThreadsEvent, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; import { ResourceLabels } from 'vs/workbench/browser/labels'; import { CommentsList, COMMENTS_VIEW_TITLE, Filter } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; import { IViewPaneOptions, FilterViewPane } from 'vs/workbench/browser/parts/views/viewPane'; @@ -192,10 +191,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this._register(this.commentService.onDidUpdateCommentThreads(this.onCommentsUpdated, this)); this._register(this.commentService.onDidDeleteDataProvider(this.onDataProviderDeleted, this)); - const styleElement = dom.createStyleSheet(container); - this.applyStyles(styleElement); - this._register(this.themeService.onDidColorThemeChange(_ => this.applyStyles(styleElement))); - this._register(this.onDidChangeBodyVisibility(visible => { if (visible) { this.refresh(); @@ -220,33 +215,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } } - private applyStyles(styleElement: HTMLStyleElement) { - const content: string[] = []; - - const theme = this.themeService.getColorTheme(); - const linkColor = theme.getColor(textLinkForeground); - if (linkColor) { - content.push(`.comments-panel .comments-panel-container a { color: ${linkColor}; }`); - } - - const linkActiveColor = theme.getColor(textLinkActiveForeground); - if (linkActiveColor) { - content.push(`.comments-panel .comments-panel-container a:hover, a:active { color: ${linkActiveColor}; }`); - } - - const focusColor = theme.getColor(focusBorder); - if (focusColor) { - content.push(`.comments-panel .comments-panel-container a:focus { outline-color: ${focusColor}; }`); - } - - const codeTextForegroundColor = theme.getColor(textPreformatForeground); - if (codeTextForegroundColor) { - content.push(`.comments-panel .comments-panel-container .text code { color: ${codeTextForegroundColor}; }`); - } - - styleElement.textContent = content.join('\n'); - } - private async renderComments(): Promise { this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads()); this.renderMessage(); diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css index a349ec5249098..a1132e43d4935 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -36,6 +36,11 @@ overflow: hidden; } +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata { + flex: 1; + display: flex; +} + .comments-panel .count, .comments-panel .user { padding-right: 5px; @@ -117,3 +122,34 @@ .comments-panel .hide { display: none; } + +.comments-panel .comments-panel-container .text a { + color: var(--vscode-textLink-foreground); +} + +.comments-panel .comments-panel-container .text a:hover, +.comments-panel .comments-panel-container a:active { + color: var(--vscode-textLink-activeForeground); +} + +.comments-panel .comments-panel-container .text a:focus { + outline-color: var(--vscode-focusBorder); +} + +.comments-panel .comments-panel-container .text code { + color: var(--vscode-textPreformat-foreground); +} + +.comments-panel .comments-panel-container .actions { + display: none; +} + +.comments-panel .comments-panel-container .actions .action-label { + padding: 2px; +} + +.comments-panel .monaco-list .monaco-list-row:hover .comment-metadata-container .actions, +.comments-panel .monaco-list .monaco-list-row.selected .comment-metadata-container .actions, +.comments-panel .monaco-list .monaco-list-row.focused .comment-metadata-container .actions { + display: block; +} diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index 9a6d878637259..e4418a28dfc7d 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -8,29 +8,26 @@ import { IRange } from 'vs/editor/common/core/range'; import { Comment, CommentThread, CommentThreadChangedEvent, CommentThreadState } from 'vs/editor/common/languages'; export interface ICommentThreadChangedEvent extends CommentThreadChangedEvent { + uniqueOwner: string; owner: string; ownerLabel: string; } export class CommentNode { - owner: string; - threadId: string; - range: IRange | undefined; - comment: Comment; + isRoot: boolean = false; replies: CommentNode[] = []; - resource: URI; - isRoot: boolean; - threadState?: CommentThreadState; - constructor(owner: string, threadId: string, resource: URI, comment: Comment, range: IRange | undefined, threadState: CommentThreadState | undefined) { - this.owner = owner; - this.threadId = threadId; - this.comment = comment; - this.resource = resource; - this.range = range; - this.isRoot = false; - this.threadState = threadState; - } + constructor( + public readonly uniqueOwner: string, + public readonly threadId: string, + public readonly resource: URI, + public readonly comment: Comment, + public readonly range: IRange | undefined, + public readonly threadState: CommentThreadState | undefined, + public readonly contextValue: string | undefined, + public readonly owner: string, + public readonly controllerHandle: number, + public readonly threadHandle: number) { } hasReply(): boolean { return this.replies && this.replies.length !== 0; @@ -39,21 +36,23 @@ export class CommentNode { export class ResourceWithCommentThreads { id: string; + uniqueOwner: string; owner: string; ownerLabel: string | undefined; commentThreads: CommentNode[]; // The top level comments on the file. Replys are nested under each node. resource: URI; - constructor(owner: string, resource: URI, commentThreads: CommentThread[]) { + constructor(uniqueOwner: string, owner: string, resource: URI, commentThreads: CommentThread[]) { + this.uniqueOwner = uniqueOwner; this.owner = owner; this.id = resource.toString(); this.resource = resource; - this.commentThreads = commentThreads.filter(thread => thread.comments && thread.comments.length).map(thread => ResourceWithCommentThreads.createCommentNode(owner, resource, thread)); + this.commentThreads = commentThreads.filter(thread => thread.comments && thread.comments.length).map(thread => ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, resource, thread)); } - public static createCommentNode(owner: string, resource: URI, commentThread: CommentThread): CommentNode { + public static createCommentNode(uniqueOwner: string, owner: string, resource: URI, commentThread: CommentThread): CommentNode { const { threadId, comments, range } = commentThread; - const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(owner, threadId, resource, comment, range, commentThread.state)); + const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(uniqueOwner, threadId, resource, comment, range, commentThread.state, commentThread.contextValue, owner, commentThread.controllerHandle, commentThread.commentThreadHandle)); if (commentNodes.length > 1) { commentNodes[0].replies = commentNodes.slice(1, commentNodes.length); } diff --git a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts index a3e171b9d40da..cd5f0ddf60c5f 100644 --- a/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts +++ b/src/vs/workbench/contrib/comments/test/browser/commentsView.test.ts @@ -49,6 +49,7 @@ class TestCommentThread implements CommentThread { class TestCommentController implements ICommentController { id: string = 'test'; label: string = 'Test Comments'; + owner: string = 'test'; features = {}; createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined): Promise { throw new Error('Method not implemented.'); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts index bf0fe703109c2..1c42939cab409 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts @@ -141,7 +141,7 @@ export class CellComments extends CellContentPart { if (this.notebookEditor.hasModel()) { const commentInfos = coalesce(await this.commentService.getNotebookComments(element.uri)); if (commentInfos.length && commentInfos[0].threads.length) { - return { owner: commentInfos[0].owner, thread: commentInfos[0].threads[0] }; + return { owner: commentInfos[0].uniqueOwner, thread: commentInfos[0].threads[0] }; } } diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 31c61213f4d5d..c43a6eb3bedcf 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -290,6 +290,12 @@ const apiMenus: IAPIMenu[] = [ description: localize('comment.commentContext', "The contributed comment context menu, rendered as a right click menu on the an individual comment in the comment thread's peek view."), proposed: 'contribCommentPeekContext' }, + { + key: 'commentsView/commentThread/context', + id: MenuId.CommentsViewThreadActions, + description: localize('commentsView.threadActions', "The contributed comment thread context menu in the comments view"), + proposed: 'contribCommentsViewThreadMenus' + }, { key: 'notebook/toolbar', id: MenuId.NotebookToolbar, diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 8571e00fc4d31..67c26906df369 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -27,6 +27,7 @@ export const allApiProposals = Object.freeze({ contribCommentEditorActionsMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts', contribCommentPeekContext: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts', contribCommentThreadAdditionalMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts', + contribCommentsViewThreadMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts', contribEditSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts', contribEditorContentMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditorContentMenu.d.ts', contribIssueReporter: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribIssueReporter.d.ts', diff --git a/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts b/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts new file mode 100644 index 0000000000000..9dc199c51ccfc --- /dev/null +++ b/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// empty placeholder declaration for the `commentsView/commentThread/context` menu contribution point