diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index bcbf6ed0363..2c3925b4d45 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1774,6 +1774,14 @@ export enum CommentThreadState { Resolved = 1 } +/** + * @internal + */ +export enum CommentThreadApplicability { + Current = 0, + Outdated = 1 +} + /** * @internal */ @@ -1811,6 +1819,7 @@ export interface CommentThread { initialCollapsibleState?: CommentThreadCollapsibleState; onDidChangeInitialCollapsibleState: Event; state?: CommentThreadState; + applicability?: CommentThreadApplicability; canReply: boolean; input?: CommentInput; onDidChangeInput: Event; diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index a5bfd4f0d49..5bfadbbc409 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -151,6 +151,20 @@ export class MainThreadCommentThread implements languages.CommentThread { this._onDidChangeState.fire(this._state); } + private _applicability: languages.CommentThreadApplicability | undefined; + + get applicability(): languages.CommentThreadApplicability | undefined { + return this._applicability; + } + + set applicability(value: languages.CommentThreadApplicability | undefined) { + this._applicability = value; + this._onDidChangeApplicability.fire(value); + } + + private readonly _onDidChangeApplicability = new Emitter(); + readonly onDidChangeApplicability: Event = this._onDidChangeApplicability.event; + public get isTemplate(): boolean { return this._isTemplate; } @@ -185,6 +199,7 @@ export class MainThreadCommentThread implements languages.CommentThread { if (modified('collapseState')) { this.initialCollapsibleState = changes.collapseState; } if (modified('canReply')) { this.canReply = changes.canReply!; } if (modified('state')) { this.state = changes.state!; } + if (modified('applicability')) { this.applicability = changes.applicability!; } if (modified('isTemplate')) { this._isTemplate = changes.isTemplate!; } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 0ffba84522d..104e52279a9 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1503,6 +1503,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CommentState: extHostTypes.CommentState, CommentThreadCollapsibleState: extHostTypes.CommentThreadCollapsibleState, CommentThreadState: extHostTypes.CommentThreadState, + CommentThreadApplicability: extHostTypes.CommentThreadApplicability, CompletionItem: extHostTypes.CompletionItem, CompletionItemKind: extHostTypes.CompletionItemKind, CompletionItemTag: extHostTypes.CompletionItemTag, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 13be2bb8b60..664d03ac70c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -135,6 +135,7 @@ export type CommentThreadChanges = Partial<{ collapseState: languages.CommentThreadCollapsibleState; canReply: boolean; state: languages.CommentThreadState; + applicability: languages.CommentThreadApplicability; isTemplate: boolean; }>; diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index eb85e826f5f..72cf9d3de72 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -265,6 +265,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo canReply: boolean; state: vscode.CommentThreadState; isTemplate: boolean; + applicability: vscode.CommentThreadApplicability; }>; class ExtHostCommentThread implements vscode.CommentThread2 { @@ -368,15 +369,21 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo this._onDidUpdateCommentThread.fire(); } - private _state?: vscode.CommentThreadState; + private _state?: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability }; - get state(): vscode.CommentThreadState { + get state(): vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined { return this._state!; } - set state(newState: vscode.CommentThreadState) { + set state(newState: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability }) { this._state = newState; - this.modifications.state = newState; + if (typeof newState === 'object') { + checkProposedApiEnabled(this.extensionDescription, 'commentThreadApplicability'); + this.modifications.state = newState.resolved; + this.modifications.applicability = newState.applicability; + } else { + this.modifications.state = newState; + } this._onDidUpdateCommentThread.fire(); } @@ -454,8 +461,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo set contextValue(value: string | undefined) { that.contextValue = value; }, get label() { return that.label; }, set label(value: string | undefined) { that.label = value; }, - get state() { return that.state; }, - set state(value: vscode.CommentThreadState) { that.state = value; }, + get state(): vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined { return that.state; }, + set state(value: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability }) { that.state = value; }, dispose: () => { that.dispose(); } @@ -510,6 +517,9 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo if (modified('state')) { formattedModifications.state = convertToState(this._state); } + if (modified('applicability')) { + formattedModifications.applicability = convertToRelevance(this._state); + } if (modified('isTemplate')) { formattedModifications.isTemplate = this._isTemplate; } @@ -766,9 +776,16 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return languages.CommentThreadCollapsibleState.Collapsed; } - function convertToState(kind: vscode.CommentThreadState | undefined): languages.CommentThreadState { - if (kind !== undefined) { - switch (kind) { + function convertToState(kind: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined): languages.CommentThreadState { + let resolvedKind: vscode.CommentThreadState | undefined; + if (typeof kind === 'object') { + resolvedKind = kind.resolved; + } else { + resolvedKind = kind; + } + + if (resolvedKind !== undefined) { + switch (resolvedKind) { case types.CommentThreadState.Unresolved: return languages.CommentThreadState.Unresolved; case types.CommentThreadState.Resolved: @@ -778,5 +795,22 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return languages.CommentThreadState.Unresolved; } + function convertToRelevance(kind: vscode.CommentThreadState | { resolved?: vscode.CommentThreadState; applicability?: vscode.CommentThreadApplicability } | undefined): languages.CommentThreadApplicability { + let applicabilityKind: vscode.CommentThreadApplicability | undefined = undefined; + if (typeof kind === 'object') { + applicabilityKind = kind.applicability; + } + + if (applicabilityKind !== undefined) { + switch (applicabilityKind) { + case types.CommentThreadApplicability.Current: + return languages.CommentThreadApplicability.Current; + case types.CommentThreadApplicability.Outdated: + return languages.CommentThreadApplicability.Outdated; + } + } + return languages.CommentThreadApplicability.Current; + } + return new ExtHostCommentsImpl(); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 1f2ac5a88a4..cd118265d5e 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3272,6 +3272,11 @@ export enum CommentThreadState { Resolved = 1 } +export enum CommentThreadApplicability { + Current = 0, + Outdated = 1 +} + //#endregion //#region Semantic Coloring diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 147d7ed6fab..1c0c588d215 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -22,7 +22,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { commentViewThreadStateColorVar, getCommentThreadStateIconColor } from 'vs/workbench/contrib/comments/browser/commentColors'; -import { CommentThreadState } from 'vs/editor/common/languages'; +import { CommentThreadApplicability, CommentThreadState } from 'vs/editor/common/languages'; import { Color } from 'vs/base/common/color'; import { IMatch } from 'vs/base/common/filters'; import { FilterOptions } from 'vs/workbench/contrib/comments/browser/commentsFilterOptions'; @@ -56,6 +56,7 @@ interface IResourceTemplateData { interface ICommentThreadTemplateData { threadMetadata: { + relevance: HTMLElement; icon: HTMLElement; userNames: HTMLSpanElement; timestamp: TimestampWidget; @@ -194,7 +195,6 @@ export class CommentNodeRenderer implements IListRenderer ) { } renderTemplate(container: HTMLElement) { - 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')); @@ -202,6 +202,7 @@ export class CommentNodeRenderer implements IListRenderer icon: dom.append(metadata, dom.$('.icon')), userNames: dom.append(metadata, dom.$('.user')), timestamp: new TimestampWidget(this.configurationService, dom.append(metadata, dom.$('.timestamp-container'))), + relevance: dom.append(metadata, dom.$('.relevance')), separator: dom.append(metadata, dom.$('.separator')), commentPreview: dom.append(metadata, dom.$('.text')), range: dom.append(metadata, dom.$('.range')) @@ -267,6 +268,16 @@ export class CommentNodeRenderer implements IListRenderer templateData.actionBar.clear(); const commentCount = node.element.replies.length + 1; + if (node.element.threadRelevance === CommentThreadApplicability.Outdated) { + templateData.threadMetadata.relevance.style.display = ''; + templateData.threadMetadata.relevance.innerText = nls.localize('outdated', "Outdated"); + templateData.threadMetadata.separator.style.display = 'none'; + } else { + templateData.threadMetadata.relevance.innerText = ''; + templateData.threadMetadata.relevance.style.display = 'none'; + templateData.threadMetadata.separator.style.display = ''; + } + templateData.threadMetadata.icon.classList.remove(...Array.from(templateData.threadMetadata.icon.classList.values()) .filter(value => value.startsWith('codicon'))); templateData.threadMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(this.getIcon(node.element.threadState))); diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index b45f12a0cb7..4a49f52f5b8 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -28,7 +28,7 @@ import { CommentsFilters, CommentsFiltersChangeEvent } from 'vs/workbench/contri import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { FilterOptions } from 'vs/workbench/contrib/comments/browser/commentsFilterOptions'; -import { CommentThreadState } from 'vs/editor/common/languages'; +import { CommentThreadApplicability, CommentThreadState } from 'vs/editor/common/languages'; import { ITreeElement } from 'vs/base/browser/ui/tree/tree'; import { Iterable } from 'vs/base/common/iterator'; import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController'; @@ -260,6 +260,46 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this.messageBoxContainer.classList.toggle('hidden', this.commentService.commentsModel.hasCommentThreads()); } + private getAriaForNode(element: CommentNode) { + if (element.range) { + if (element.threadRelevance === CommentThreadApplicability.Outdated) { + return nls.localize('resourceWithCommentLabelOutdated', + "Outdated from ${0} at line {1} column {2} in {3}, source: {4}", + element.comment.userName, + element.range.startLineNumber, + element.range.startColumn, + basename(element.resource), + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value + ); + } else { + return nls.localize('resourceWithCommentLabel', + "${0} at line {1} column {2} in {3}, source: {4}", + element.comment.userName, + element.range.startLineNumber, + element.range.startColumn, + basename(element.resource), + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value + ); + } + } else { + if (element.threadRelevance === CommentThreadApplicability.Outdated) { + return nls.localize('resourceWithCommentLabelFileOutdated', + "Outdated from {0} in {1}, source: {2}", + element.comment.userName, + basename(element.resource), + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value + ); + } else { + return nls.localize('resourceWithCommentLabelFile', + "{0} in {1}, source: {2}", + element.comment.userName, + basename(element.resource), + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value + ); + } + } + } + private createTree(): void { this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); this.tree = this._register(this.instantiationService.createInstance(CommentsList, this.treeLabels, this.treeContainer, { @@ -272,7 +312,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } }, accessibilityProvider: { - getAriaLabel(element: any): string { + getAriaLabel: (element: any): string => { if (element instanceof CommentsModel) { return nls.localize('rootCommentsLabel', "Comments for current workspace"); } @@ -280,23 +320,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { return nls.localize('resourceWithCommentThreadsLabel', "Comments in {0}, full path {1}", basename(element.resource), element.resource.fsPath); } if (element instanceof CommentNode) { - if (element.range) { - return nls.localize('resourceWithCommentLabel', - "${0} at line {1} column {2} in {3}, source: {4}", - element.comment.userName, - element.range.startLineNumber, - element.range.startColumn, - basename(element.resource), - (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value - ); - } else { - return nls.localize('resourceWithCommentLabelFile', - "${0} in {1}, source: {2}", - element.comment.userName, - basename(element.resource), - (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value - ); - } + return this.getAriaForNode(element); } return ''; }, diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css index a1132e43d49..938c658fd2d 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -53,10 +53,23 @@ } .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .count, +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata-container .relevance, .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata-container .user { min-width: fit-content; } +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata-container .relevance { + border-radius: 2px; + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + padding: 0px 4px 1px 4px; + font-size: 0.9em; + margin-right: 4px; + margin-top: 4px; + margin-bottom: 3px; + line-height: 14px; +} + .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .text { display: flex; flex: 1; diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index 7bf9efe417a..fbf25f6c06b 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; -import { Comment, CommentThread, CommentThreadChangedEvent, CommentThreadState } from 'vs/editor/common/languages'; +import { Comment, CommentThread, CommentThreadChangedEvent, CommentThreadApplicability, CommentThreadState } from 'vs/editor/common/languages'; export interface ICommentThreadChangedEvent extends CommentThreadChangedEvent { uniqueOwner: string; @@ -19,6 +19,7 @@ export class CommentNode { public readonly threadId: string; public readonly range: IRange | undefined; public readonly threadState: CommentThreadState | undefined; + public readonly threadRelevance: CommentThreadApplicability | undefined; public readonly contextValue: string | undefined; public readonly controllerHandle: number; public readonly threadHandle: number; @@ -32,6 +33,7 @@ export class CommentNode { this.threadId = thread.threadId; this.range = thread.range; this.threadState = thread.state; + this.threadRelevance = thread.applicability; this.contextValue = thread.contextValue; this.controllerHandle = thread.controllerHandle; this.threadHandle = thread.commentThreadHandle; diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 548f66575c0..804e91a26e8 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -22,6 +22,7 @@ export const allApiProposals = Object.freeze({ codeActionRanges: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codeActionRanges.d.ts', codiconDecoration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', commentReactor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReactor.d.ts', + commentThreadApplicability: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts', commentingRangeHint: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentingRangeHint.d.ts', commentsDraftState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsDraftState.d.ts', contribCommentEditorActionsMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts', diff --git a/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts b/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts new file mode 100644 index 00000000000..e09f5a34d6b --- /dev/null +++ b/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // @alexr00 https://github.com/microsoft/vscode/issues/207402 + + export enum CommentThreadApplicability { + Current = 0, + Outdated = 1 + } + + export interface CommentThread2 { + /* @api this is a bit weird for the extension now. The CommentThread is a managed object, which means it listens + * to when it's properties are set, but not if it's properties are modified. This means that this will not work to update the resolved state + * + * thread.state.resolved = CommentThreadState.Resolved; + * + * but this will work + * + * thread.state = { + * resolved: CommentThreadState.Resolved + * applicability: thread.state.applicability + * }; + * + * Worth noting that we already have this problem for the `comments` property. + */ + state?: CommentThreadState | { resolved?: CommentThreadState; applicability?: CommentThreadApplicability }; + } +} diff --git a/src/vscode-dts/vscode.proposed.fileComments.d.ts b/src/vscode-dts/vscode.proposed.fileComments.d.ts index 09c729145f2..7370f22f762 100644 --- a/src/vscode-dts/vscode.proposed.fileComments.d.ts +++ b/src/vscode-dts/vscode.proposed.fileComments.d.ts @@ -59,10 +59,8 @@ declare module 'vscode' { */ label?: string; - /** - * The optional state of a comment thread, which may affect how the comment is displayed. - */ - state?: CommentThreadState; + // from the commentThreadRelevance proposal + state?: CommentThreadState | { resolved?: CommentThreadState; applicability?: CommentThreadApplicability }; /** * Dispose this comment thread.