Allow comment threads to provide user info for comment replies (#246187)

Part of #246088
This commit is contained in:
Alex Ross
2025-04-10 12:05:56 +02:00
committed by GitHub
parent 6544995708
commit 807a46072c
10 changed files with 98 additions and 44 deletions
+1 -1
View File
@@ -2066,7 +2066,7 @@ export interface CommentThread<T = IRange> {
onDidChangeInitialCollapsibleState: Event<CommentThreadCollapsibleState | undefined>;
state?: CommentThreadState;
applicability?: CommentThreadApplicability;
canReply: boolean;
canReply: boolean | CommentAuthorInformation;
input?: CommentInput;
onDidChangeInput: Event<CommentInput | undefined>;
onDidChangeLabel: Event<string | undefined>;
@@ -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',
},
@@ -93,9 +93,9 @@ export class MainThreadCommentThread<T> implements languages.CommentThread<T> {
private readonly _onDidChangeCanReply = new Emitter<boolean>();
get onDidChangeCanReply(): Event<boolean> { 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<T> implements languages.CommentThread<T> {
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
) {
@@ -141,7 +141,7 @@ export type CommentThreadChanges<T = IRange> = Partial<{
contextValue: string | null;
comments: CommentChanges[];
collapseState: languages.CommentThreadCollapsibleState;
canReply: boolean;
canReply: boolean | languages.CommentAuthorInformation;
state: languages.CommentThreadState;
applicability: languages.CommentThreadApplicability;
isTemplate: boolean;
@@ -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; },
@@ -223,7 +223,7 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable {
private updateCommentUserIcon(userIconPath: UriComponents | undefined) {
this._avatar.textContent = '';
if (userIconPath) {
const img = <HTMLImageElement>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();
}
@@ -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<T extends IRange | ICellRange> extends Disposable {
commentEditor: ICodeEditor;
form: HTMLElement;
private _container: HTMLElement;
private _form: HTMLElement;
commentEditorIsEmpty: IContextKey<boolean>;
private avatar!: HTMLElement;
private _error!: HTMLElement;
private _formActions!: HTMLElement;
private _editorActions!: HTMLElement;
@@ -70,16 +72,18 @@ export class CommentReply<T extends IRange | ICellRange> 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<T extends IRange | ICellRange> extends Disposable {
}
}));
this.createTextModelListener(this.commentEditor, this.form);
this.createTextModelListener(this.commentEditor, this._form);
this.setCommentEditorDecorations();
@@ -123,12 +127,12 @@ export class CommentReply<T extends IRange | ICellRange> 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<T extends IRange | ICellRange> 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<T extends IRange | ICellRange> 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<T extends IRange | ICellRange> 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<T extends IRange | ICellRange> 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');
}
@@ -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;
+22
View File
@@ -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;
}
}
@@ -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;