diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index e90682e529f..b41762c603a 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -2066,7 +2066,7 @@ export interface CommentThread { onDidChangeInitialCollapsibleState: Event; state?: CommentThreadState; applicability?: CommentThreadApplicability; - canReply: boolean; + canReply: boolean | CommentAuthorInformation; input?: CommentInput; onDidChangeInput: Event; onDidChangeLabel: Event; diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 5bd61a59af8..587d9e1e8d2 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -62,6 +62,9 @@ const _allApiProposals = { commentReactor: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReactor.d.ts', }, + commentReplyAuthor: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReplyAuthor.d.ts', + }, commentReveal: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReveal.d.ts', }, diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index b13b12de203..74890c18ebc 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -93,9 +93,9 @@ export class MainThreadCommentThread implements languages.CommentThread { private readonly _onDidChangeCanReply = new Emitter(); get onDidChangeCanReply(): Event { return this._onDidChangeCanReply.event; } - set canReply(state: boolean) { + set canReply(state: boolean | languages.CommentAuthorInformation) { this._canReply = state; - this._onDidChangeCanReply.fire(this._canReply); + this._onDidChangeCanReply.fire(!!this._canReply); } get canReply() { @@ -182,7 +182,7 @@ export class MainThreadCommentThread implements languages.CommentThread { public resource: string, private _range: T | undefined, comments: languages.Comment[] | undefined, - private _canReply: boolean, + private _canReply: boolean | languages.CommentAuthorInformation, private _isTemplate: boolean, public editorId?: string ) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 78a85679862..c37fb341e52 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -141,7 +141,7 @@ export type CommentThreadChanges = Partial<{ contextValue: string | null; comments: CommentChanges[]; collapseState: languages.CommentThreadCollapsibleState; - canReply: boolean; + canReply: boolean | languages.CommentAuthorInformation; 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 908d094ec00..6605a713d23 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -268,7 +268,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo contextValue: string | undefined; comments: vscode.Comment[]; collapsibleState: vscode.CommentThreadCollapsibleState; - canReply: boolean; + canReply: boolean | vscode.CommentAuthorInformation; state: vscode.CommentThreadState; isTemplate: boolean; applicability: vscode.CommentThreadApplicability; @@ -316,9 +316,13 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return this._range; } - private _canReply: boolean = true; + private _canReply: boolean | vscode.CommentAuthorInformation = true; + + set canReply(state: boolean | vscode.CommentAuthorInformation) { + if (typeof state !== 'boolean') { + checkProposedApiEnabled(this.extensionDescription, 'commentReplyAuthor'); + } - set canReply(state: boolean) { if (this._canReply !== state) { this._canReply = state; this.modifications.canReply = state; @@ -465,7 +469,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo get collapsibleState() { return that.collapsibleState; }, set collapsibleState(value: vscode.CommentThreadCollapsibleState) { that.collapsibleState = value; }, get canReply() { return that.canReply; }, - set canReply(state: boolean) { that.canReply = state; }, + set canReply(state: boolean | vscode.CommentAuthorInformation) { that.canReply = state; }, get contextValue() { return that.contextValue; }, set contextValue(value: string | undefined) { that.contextValue = value; }, get label() { return that.label; }, diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 77f5e4117a2..5051d1a849a 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -223,7 +223,7 @@ export class CommentNode extends Disposable { private updateCommentUserIcon(userIconPath: UriComponents | undefined) { this._avatar.textContent = ''; if (userIconPath) { - const img = dom.append(this._avatar, dom.$('img.avatar')); + const img = dom.append(this._avatar, dom.$('img.avatar')) as HTMLImageElement; img.src = FileAccess.uriToBrowserUri(URI.revive(userIconPath)).toString(true); img.onerror = _ => img.remove(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 917b6a64239..b181a9cb5c1 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -9,7 +9,7 @@ import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../base/browser/ui/mo import { IAction } from '../../../../base/common/actions.js'; import { Disposable, IDisposable, dispose } from '../../../../base/common/lifecycle.js'; import { MarshalledId } from '../../../../base/common/marshallingIds.js'; -import { Schemas } from '../../../../base/common/network.js'; +import { FileAccess, Schemas } from '../../../../base/common/network.js'; import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; @@ -38,8 +38,10 @@ export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; export class CommentReply extends Disposable { commentEditor: ICodeEditor; - form: HTMLElement; + private _container: HTMLElement; + private _form: HTMLElement; commentEditorIsEmpty: IContextKey; + private avatar!: HTMLElement; private _error!: HTMLElement; private _formActions!: HTMLElement; private _editorActions!: HTMLElement; @@ -70,16 +72,18 @@ export class CommentReply extends Disposable { @ITextModelService private readonly textModelService: ITextModelService ) { super(); - - this.form = dom.append(container, dom.$('.comment-form')); - this.commentEditor = this._register(this._scopedInstatiationService.createInstance(SimpleCommentEditor, this.form, SimpleCommentEditor.getEditorOptions(configurationService), _contextKeyService, this._parentThread)); + this._container = dom.append(container, dom.$('.comment-form-container')); + this._form = dom.append(this._container, dom.$('.comment-form')); + this.commentEditor = this._register(this._scopedInstatiationService.createInstance(SimpleCommentEditor, this._form, SimpleCommentEditor.getEditorOptions(configurationService), _contextKeyService, this._parentThread)); this.commentEditorIsEmpty = CommentContextKeys.commentIsEmpty.bindTo(this._contextKeyService); this.commentEditorIsEmpty.set(!this._pendingComment); this.initialize(focus); } - async initialize(focus: boolean) { + private async initialize(focus: boolean) { + this.avatar = dom.append(this._form, dom.$('.avatar-container')); + this.updateAuthorInfo(); const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID); const params = JSON.stringify({ @@ -115,7 +119,7 @@ export class CommentReply extends Disposable { } })); - this.createTextModelListener(this.commentEditor, this.form); + this.createTextModelListener(this.commentEditor, this._form); this.setCommentEditorDecorations(); @@ -123,12 +127,12 @@ export class CommentReply extends Disposable { if (this._pendingComment) { this.expandReplyArea(); } else if (hasExistingComments) { - this.createReplyButton(this.commentEditor, this.form); + this.createReplyButton(this.commentEditor, this._form); } else if (focus && (this._commentThread.comments && this._commentThread.comments.length === 0)) { this.expandReplyArea(); } - this._error = dom.append(this.form, dom.$('.validation-error.hidden')); - const formActions = dom.append(this.form, dom.$('.form-actions')); + this._error = dom.append(this._container, dom.$('.validation-error.hidden')); + const formActions = dom.append(this._container, dom.$('.form-actions')); this._formActions = dom.append(formActions, dom.$('.other-actions')); this.createCommentWidgetFormActions(this._formActions, model.object.textEditorModel); this._editorActions = dom.append(formActions, dom.$('.editor-actions')); @@ -149,7 +153,7 @@ export class CommentReply extends Disposable { const oldAndNewBothEmpty = !this._commentThread.comments?.length && !commentThread.comments?.length; if (!this._reviewThreadReplyButton) { - this.createReplyButton(this.commentEditor, this.form); + this.createReplyButton(this.commentEditor, this._form); } if (this._commentThread.comments && this._commentThread.comments.length === 0 && !oldAndNewBothEmpty) { @@ -203,11 +207,23 @@ export class CommentReply extends Disposable { return this.commentEditor.hasWidgetFocus(); } - public updateCanReply() { - if (!this._commentThread.canReply) { - this.form.style.display = 'none'; + private updateAuthorInfo() { + this.avatar.textContent = ''; + if (typeof this._commentThread.canReply !== 'boolean' && this._commentThread.canReply.iconPath) { + this.avatar.style.display = 'block'; + const img = dom.append(this.avatar, dom.$('img.avatar')) as HTMLImageElement; + img.src = FileAccess.uriToBrowserUri(URI.revive(this._commentThread.canReply.iconPath)).toString(true); } else { - this.form.style.display = 'block'; + this.avatar.style.display = 'none'; + } + } + + public updateCanReply() { + this.updateAuthorInfo(); + if (!this._commentThread.canReply) { + this._container.style.display = 'none'; + } else { + this._container.style.display = 'block'; } } @@ -320,12 +336,12 @@ export class CommentReply extends Disposable { } private get isReplyExpanded(): boolean { - return this.form.classList.contains('expand'); + return this._container.classList.contains('expand'); } private expandReplyArea() { if (!this.isReplyExpanded) { - this.form.classList.add('expand'); + this._container.classList.add('expand'); this.commentEditor.focus(); this.commentEditor.layout(); } @@ -345,7 +361,7 @@ export class CommentReply extends Disposable { } this.commentEditor.setValue(''); this._pendingComment = { body: '', cursor: new Position(1, 1) }; - this.form.classList.remove('expand'); + this._container.classList.remove('expand'); this._error.textContent = ''; this._error.classList.add('hidden'); } diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 53832daad0a..f0b9a064d8d 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -78,7 +78,7 @@ margin-top: 4px !important; } -.review-widget .body .review-comment .avatar-container img.avatar { +.review-widget .body .avatar-container img.avatar { height: 28px; width: 28px; display: inline-block; @@ -246,7 +246,7 @@ max-width: 100%; } -.review-widget .body .comment-form { +.review-widget .body .comment-form-container { margin: 8px 20px; } @@ -296,7 +296,6 @@ padding: 4px 10px; } - .review-widget .body .comment-additional-actions .codicon-drop-down-button { align-items: center; } @@ -304,17 +303,27 @@ .review-widget .body .monaco-editor { color: var(--vscode-editor-foreground); } -.review-widget .body .comment-form.expand .review-thread-reply-button { + +.review-widget .body .comment-form-container .comment-form { + display: flex; + flex-direction: row; +} + +.review-widget .body .comment-form-container .comment-form .avatar-container { + padding-right: 20px; +} + +.review-widget .body .comment-form-container.expand .review-thread-reply-button { display: none; } -.review-widget .body .comment-form.expand .monaco-editor, -.review-widget .body .comment-form.expand .form-actions { +.review-widget .body .comment-form-container.expand .monaco-editor, +.review-widget .body .comment-form-container.expand .form-actions { display: block; box-sizing: content-box; } -.review-widget .body .comment-form .review-thread-reply-button { +.review-widget .body .comment-form-container .review-thread-reply-button { text-align: left; display: block; width: 100%; @@ -333,18 +342,18 @@ font-family: var(--monaco-monospace-font); } -.review-widget .body .comment-form .review-thread-reply-button:focus { +.review-widget .body .comment-form-container .review-thread-reply-button:focus { outline-style: solid; outline-width: 1px; } -.review-widget .body .comment-form .monaco-editor, -.review-widget .body .comment-form .monaco-editor .monaco-editor-background, +.review-widget .body .comment-form-container .monaco-editor, +.review-widget .body .comment-form-container .monaco-editor .monaco-editor-background, .review-widget .body .edit-container .monaco-editor .monaco-editor-background { background-color: var(--vscode-editorCommentsWidget-replyInputBackground); } -.review-widget .body .comment-form .monaco-editor, +.review-widget .body .comment-form-container .monaco-editor, .review-widget .body .edit-container .monaco-editor { width: 100%; min-height: 90px; @@ -355,12 +364,12 @@ padding: 6px 0 6px 12px; } -.review-widget .body .comment-form .monaco-editor, -.review-widget .body .comment-form .form-actions { +.review-widget .body .comment-form-container .monaco-editor, +.review-widget .body .comment-form-container .form-actions { display: none; } -.review-widget .body .comment-form .form-actions, +.review-widget .body .comment-form-container .form-actions, .review-widget .body .edit-container .form-actions { overflow: auto; margin: 10px 0; @@ -375,7 +384,7 @@ margin-right: 12px; } -.review-widget .body .comment-form .form-actions .monaco-text-button, +.review-widget .body .comment-form-container .form-actions .monaco-text-button, .review-widget .body .edit-container .monaco-text-button { width: auto; padding: 4px 10px; diff --git a/src/vscode-dts/vscode.proposed.commentReplyAuthor.d.ts b/src/vscode-dts/vscode.proposed.commentReplyAuthor.d.ts new file mode 100644 index 00000000000..d91462b7dd8 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.commentReplyAuthor.d.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * 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/246088 + + export interface CommentThread2 { + canReply: boolean | CommentAuthorInformation; + + readonly uri: Uri; + range: Range | undefined; + comments: readonly Comment[]; + collapsibleState: CommentThreadCollapsibleState; + contextValue?: string; + label?: string; + state?: CommentThreadState | { resolved?: CommentThreadState; applicability?: CommentThreadApplicability }; + dispose(): void; + } +} diff --git a/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts b/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts index fb99abb48bd..772771eef77 100644 --- a/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts +++ b/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts @@ -32,7 +32,7 @@ declare module 'vscode' { range: Range | undefined; comments: readonly Comment[]; collapsibleState: CommentThreadCollapsibleState; - canReply: boolean; + canReply: boolean | CommentAuthorInformation; contextValue?: string; label?: string; dispose(): void;