Path: blob/main/src/vs/workbench/contrib/comments/browser/commentNode.ts
5237 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as nls from '../../../../nls.js';6import * as dom from '../../../../base/browser/dom.js';7import * as languages from '../../../../editor/common/languages.js';8import { ActionsOrientation, ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';9import { Action, IAction, Separator, ActionRunner } from '../../../../base/common/actions.js';10import { Disposable, DisposableStore, IReference, MutableDisposable } from '../../../../base/common/lifecycle.js';11import { URI, UriComponents } from '../../../../base/common/uri.js';12import { IMarkdownRendererExtraOptions, IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';13import { IRenderedMarkdown } from '../../../../base/browser/markdownRenderer.js';14import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';15import { ICommentService } from './commentService.js';16import { LayoutableEditor, MIN_EDITOR_HEIGHT, SimpleCommentEditor, calculateEditorHeight } from './simpleCommentEditor.js';17import { Emitter, Event } from '../../../../base/common/event.js';18import { INotificationService } from '../../../../platform/notification/common/notification.js';19import { ToolBar } from '../../../../base/browser/ui/toolbar/toolbar.js';20import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';21import { AnchorAlignment } from '../../../../base/browser/ui/contextview/contextview.js';22import { ToggleReactionsAction, ReactionAction, ReactionActionViewItem } from './reactionsAction.js';23import { ICommentThreadWidget } from '../common/commentThreadWidget.js';24import { MenuItemAction, SubmenuItemAction, IMenu, MenuId } from '../../../../platform/actions/common/actions.js';25import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';26import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js';27import { CommentFormActions } from './commentFormActions.js';28import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../base/browser/ui/mouseCursor/mouseCursor.js';29import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';30import { DropdownMenuActionViewItem } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js';31import { Codicon } from '../../../../base/common/codicons.js';32import { ThemeIcon } from '../../../../base/common/themables.js';33import { MarshalledId } from '../../../../base/common/marshallingIds.js';34import { TimestampWidget } from './timestamp.js';35import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';36import { IMarkdownString } from '../../../../base/common/htmlContent.js';37import { IRange } from '../../../../editor/common/core/range.js';38import { ICellRange } from '../../notebook/common/notebookRange.js';39import { CommentMenus } from './commentMenus.js';40import { Scrollable, ScrollbarVisibility } from '../../../../base/common/scrollable.js';41import { SmoothScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';42import { DomEmitter } from '../../../../base/browser/event.js';43import { CommentContextKeys } from '../common/commentContextKeys.js';44import { FileAccess, Schemas } from '../../../../base/common/network.js';45import { COMMENTS_SECTION, ICommentsConfiguration } from '../common/commentsConfiguration.js';46import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';47import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';48import { MarshalledCommentThread } from '../../../common/comments.js';49import { IHoverService } from '../../../../platform/hover/browser/hover.js';50import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js';51import { Position } from '../../../../editor/common/core/position.js';5253class CommentsActionRunner extends ActionRunner {54protected override async runAction(action: IAction, context: unknown[]): Promise<void> {55await action.run(...context);56}57}5859export class CommentNode<T extends IRange | ICellRange> extends Disposable {60private _domNode: HTMLElement;61private _body: HTMLElement;62private _avatar: HTMLElement;63private readonly _md: MutableDisposable<IRenderedMarkdown> = this._register(new MutableDisposable());64private _plainText: HTMLElement | undefined;65private _clearTimeout: Timeout | null;6667private _editAction: Action | null = null;68private _commentEditContainer: HTMLElement | null = null;69private _commentDetailsContainer: HTMLElement;70private _actionsToolbarContainer!: HTMLElement;71private readonly _reactionsActionBar: MutableDisposable<ActionBar> = this._register(new MutableDisposable());72private readonly _reactionActions: DisposableStore = this._register(new DisposableStore());73private _reactionActionsContainer?: HTMLElement;74private _commentEditor: SimpleCommentEditor | null = null;75private _commentEditorModel: IReference<IResolvedTextEditorModel> | null = null;76private _editorHeight = MIN_EDITOR_HEIGHT;7778private _isPendingLabel!: HTMLElement;79private _timestamp: HTMLElement | undefined;80private _timestampWidget: TimestampWidget | undefined;81private _contextKeyService: IContextKeyService;82private _commentContextValue: IContextKey<string>;83private _commentMenus: CommentMenus;8485private _scrollable!: Scrollable;86private _scrollableElement!: SmoothScrollableElement;8788private readonly _actionRunner: CommentsActionRunner = this._register(new CommentsActionRunner());89private readonly toolbar: MutableDisposable<ToolBar> = this._register(new MutableDisposable());90private _commentFormActions: CommentFormActions | null = null;91private _commentEditorActions: CommentFormActions | null = null;9293private readonly _onDidClick = new Emitter<CommentNode<T>>();9495public get domNode(): HTMLElement {96return this._domNode;97}9899public isEditing: boolean = false;100101constructor(102private readonly parentEditor: LayoutableEditor,103private commentThread: languages.CommentThread<T>,104public comment: languages.Comment,105private pendingEdit: languages.PendingComment | undefined,106private owner: string,107private resource: URI,108private parentThread: ICommentThreadWidget,109private readonly markdownRendererOptions: IMarkdownRendererExtraOptions,110@IInstantiationService private instantiationService: IInstantiationService,111@ICommentService private commentService: ICommentService,112@INotificationService private notificationService: INotificationService,113@IContextMenuService private contextMenuService: IContextMenuService,114@IContextKeyService contextKeyService: IContextKeyService,115@IConfigurationService private configurationService: IConfigurationService,116@IHoverService private hoverService: IHoverService,117@IKeybindingService private keybindingService: IKeybindingService,118@ITextModelService private readonly textModelService: ITextModelService,119@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,120) {121super();122123this._domNode = dom.$('div.review-comment');124this._contextKeyService = this._register(contextKeyService.createScoped(this._domNode));125this._commentContextValue = CommentContextKeys.commentContext.bindTo(this._contextKeyService);126if (this.comment.contextValue) {127this._commentContextValue.set(this.comment.contextValue);128}129this._commentMenus = this.commentService.getCommentMenus(this.owner);130131this._domNode.tabIndex = -1;132this._avatar = dom.append(this._domNode, dom.$('div.avatar-container'));133this.updateCommentUserIcon(this.comment.userIconPath);134135this._commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents'));136137this.createHeader(this._commentDetailsContainer);138this._body = document.createElement(`div`);139this._body.classList.add('comment-body', MOUSE_CURSOR_TEXT_CSS_CLASS_NAME);140if (configurationService.getValue<ICommentsConfiguration | undefined>(COMMENTS_SECTION)?.maxHeight !== false) {141this._body.classList.add('comment-body-max-height');142}143144this.createScroll(this._commentDetailsContainer, this._body);145this.updateCommentBody(this.comment.body);146147this.createReactionsContainer(this._commentDetailsContainer);148149this._domNode.setAttribute('aria-label', `${comment.userName}, ${this.commentBodyValue}`);150this._domNode.setAttribute('role', 'treeitem');151this._clearTimeout = null;152153this._register(dom.addDisposableListener(this._domNode, dom.EventType.CLICK, () => this.isEditing || this._onDidClick.fire(this)));154this._register(dom.addDisposableListener(this._domNode, dom.EventType.CONTEXT_MENU, e => {155return this.onContextMenu(e);156}));157158if (pendingEdit) {159this.switchToEditMode();160}161162this.activeCommentListeners();163}164165private activeCommentListeners() {166this._register(dom.addDisposableListener(this._domNode, dom.EventType.FOCUS_IN, () => {167this.commentService.setActiveCommentAndThread(this.owner, { thread: this.commentThread, comment: this.comment });168}, true));169}170171private createScroll(container: HTMLElement, body: HTMLElement) {172this._scrollable = this._register(new Scrollable({173forceIntegerValues: true,174smoothScrollDuration: 125,175scheduleAtNextAnimationFrame: cb => dom.scheduleAtNextAnimationFrame(dom.getWindow(container), cb)176}));177this._scrollableElement = this._register(new SmoothScrollableElement(body, {178horizontal: ScrollbarVisibility.Visible,179vertical: ScrollbarVisibility.Visible180}, this._scrollable));181182this._register(this._scrollableElement.onScroll(e => {183if (e.scrollLeftChanged) {184body.scrollLeft = e.scrollLeft;185}186if (e.scrollTopChanged) {187body.scrollTop = e.scrollTop;188}189}));190191const onDidScrollViewContainer = this._register(new DomEmitter(body, 'scroll')).event;192this._register(onDidScrollViewContainer(_ => {193const position = this._scrollableElement.getScrollPosition();194const scrollLeft = Math.abs(body.scrollLeft - position.scrollLeft) <= 1 ? undefined : body.scrollLeft;195const scrollTop = Math.abs(body.scrollTop - position.scrollTop) <= 1 ? undefined : body.scrollTop;196197if (scrollLeft !== undefined || scrollTop !== undefined) {198this._scrollableElement.setScrollPosition({ scrollLeft, scrollTop });199}200}));201202container.appendChild(this._scrollableElement.getDomNode());203}204205private updateCommentBody(body: string | IMarkdownString) {206this._body.innerText = '';207this._md.clear();208this._plainText = undefined;209if (typeof body === 'string') {210this._plainText = dom.append(this._body, dom.$('.comment-body-plainstring'));211this._plainText.innerText = body;212} else {213this._md.value = this.markdownRendererService.render(body, this.markdownRendererOptions);214this._body.appendChild(this._md.value.element);215}216}217218private updateCommentUserIcon(userIconPath: UriComponents | undefined) {219this._avatar.textContent = '';220if (userIconPath) {221const img = dom.append(this._avatar, dom.$('img.avatar')) as HTMLImageElement;222img.src = FileAccess.uriToBrowserUri(URI.revive(userIconPath)).toString(true);223img.onerror = _ => img.remove();224}225}226227public get onDidClick(): Event<CommentNode<T>> {228return this._onDidClick.event;229}230231private createTimestamp(container: HTMLElement) {232this._timestamp = dom.append(container, dom.$('span.timestamp-container'));233this.updateTimestamp(this.comment.timestamp);234}235236private updateTimestamp(raw?: string) {237if (!this._timestamp) {238return;239}240241const timestamp = raw !== undefined ? new Date(raw) : undefined;242if (!timestamp) {243this._timestampWidget?.dispose();244} else {245if (!this._timestampWidget) {246this._timestampWidget = new TimestampWidget(this.configurationService, this.hoverService, this._timestamp, timestamp);247this._register(this._timestampWidget);248} else {249this._timestampWidget.setTimestamp(timestamp);250}251}252}253254private createHeader(commentDetailsContainer: HTMLElement): void {255const header = dom.append(commentDetailsContainer, dom.$(`div.comment-title.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`));256const infoContainer = dom.append(header, dom.$('comment-header-info'));257const author = dom.append(infoContainer, dom.$('strong.author'));258author.innerText = this.comment.userName;259this.createTimestamp(infoContainer);260this._isPendingLabel = dom.append(infoContainer, dom.$('span.isPending'));261262if (this.comment.label) {263this._isPendingLabel.innerText = this.comment.label;264} else {265this._isPendingLabel.innerText = '';266}267268this._actionsToolbarContainer = dom.append(header, dom.$('.comment-actions'));269this.createActionsToolbar();270}271272private getToolbarActions(menu: IMenu): { primary: IAction[]; secondary: IAction[] } {273const contributedActions = menu.getActions({ shouldForwardArgs: true });274const primary: IAction[] = [];275const secondary: IAction[] = [];276const result = { primary, secondary };277fillInActions(contributedActions, result, false, g => /^inline/.test(g));278return result;279}280281private get commentNodeContext(): [{ thread: languages.CommentThread<T>; commentUniqueId: number; $mid: MarshalledId.CommentNode }, MarshalledCommentThread] {282return [{283thread: this.commentThread,284commentUniqueId: this.comment.uniqueIdInThread,285$mid: MarshalledId.CommentNode286},287{288commentControlHandle: this.commentThread.controllerHandle,289commentThreadHandle: this.commentThread.commentThreadHandle,290$mid: MarshalledId.CommentThread291}];292}293294private createToolbar() {295this.toolbar.value = new ToolBar(this._actionsToolbarContainer, this.contextMenuService, {296actionViewItemProvider: (action, options) => {297if (action.id === ToggleReactionsAction.ID) {298return new DropdownMenuActionViewItem(299action,300(<ToggleReactionsAction>action).menuActions,301this.contextMenuService,302{303...options,304actionViewItemProvider: (action, options) => this.actionViewItemProvider(action as Action, options),305classNames: ['toolbar-toggle-pickReactions', ...ThemeIcon.asClassNameArray(Codicon.reactions)],306anchorAlignmentProvider: () => AnchorAlignment.RIGHT307}308);309}310return this.actionViewItemProvider(action as Action, options);311},312orientation: ActionsOrientation.HORIZONTAL313});314315this.toolbar.value.context = this.commentNodeContext;316this.toolbar.value.actionRunner = this._actionRunner;317}318319private createActionsToolbar() {320const actions: IAction[] = [];321322const menu = this._commentMenus.getCommentTitleActions(this.comment, this._contextKeyService);323this._register(menu);324this._register(menu.onDidChange(e => {325const { primary, secondary } = this.getToolbarActions(menu);326if (!this.toolbar && (primary.length || secondary.length)) {327this.createToolbar();328}329this.toolbar.value!.setActions(primary, secondary);330}));331332const { primary, secondary } = this.getToolbarActions(menu);333actions.push(...primary);334335if (actions.length || secondary.length) {336this.createToolbar();337this.toolbar.value!.setActions(actions, secondary);338}339}340341actionViewItemProvider(action: Action, options: IActionViewItemOptions) {342if (action.id === ToggleReactionsAction.ID) {343options = { label: false, icon: true };344} else {345options = { label: false, icon: true };346}347348if (action.id === ReactionAction.ID) {349const item = new ReactionActionViewItem(action);350return item;351} else if (action instanceof MenuItemAction) {352return this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate });353} else if (action instanceof SubmenuItemAction) {354return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, options);355} else {356const item = new ActionViewItem({}, action, options);357return item;358}359}360361async submitComment(): Promise<void> {362if (this._commentEditor && this._commentFormActions) {363await this._commentFormActions.triggerDefaultAction();364this.pendingEdit = undefined;365}366}367368private createReactionPicker(reactionGroup: languages.CommentReaction[]): ToggleReactionsAction {369const toggleReactionAction = this._reactionActions.add(new ToggleReactionsAction(() => {370toggleReactionActionViewItem?.show();371}, nls.localize('commentToggleReaction', "Toggle Reaction")));372373let reactionMenuActions: Action[] = [];374if (reactionGroup && reactionGroup.length) {375reactionMenuActions = reactionGroup.map((reaction) => {376return this._reactionActions.add(new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => {377try {378await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread, this.comment, reaction);379} catch (e) {380const error = e.message381? nls.localize('commentToggleReactionError', "Toggling the comment reaction failed: {0}.", e.message)382: nls.localize('commentToggleReactionDefaultError', "Toggling the comment reaction failed");383this.notificationService.error(error);384}385}));386});387}388389toggleReactionAction.menuActions = reactionMenuActions;390391const toggleReactionActionViewItem: DropdownMenuActionViewItem = this._reactionActions.add(new DropdownMenuActionViewItem(392toggleReactionAction,393(<ToggleReactionsAction>toggleReactionAction).menuActions,394this.contextMenuService,395{396actionViewItemProvider: (action, options) => {397if (action.id === ToggleReactionsAction.ID) {398return toggleReactionActionViewItem;399}400return this.actionViewItemProvider(action as Action, options);401},402classNames: 'toolbar-toggle-pickReactions',403anchorAlignmentProvider: () => AnchorAlignment.RIGHT404}405));406407return toggleReactionAction;408}409410private createReactionsContainer(commentDetailsContainer: HTMLElement): void {411this._reactionActionsContainer?.remove();412this._reactionsActionBar.clear();413this._reactionActions.clear();414415const hasReactionHandler = this.commentService.hasReactionHandler(this.owner);416const reactions = this.comment.commentReactions?.filter(reaction => !!reaction.count) || [];417418// Only create the container if there are reactions to show or if there's a reaction handler419if (reactions.length === 0 && !hasReactionHandler) {420return;421}422423this._reactionActionsContainer = dom.append(commentDetailsContainer, dom.$('div.comment-reactions'));424this._reactionsActionBar.value = new ActionBar(this._reactionActionsContainer, {425actionViewItemProvider: (action, options) => {426if (action.id === ToggleReactionsAction.ID) {427return new DropdownMenuActionViewItem(428action,429(<ToggleReactionsAction>action).menuActions,430this.contextMenuService,431{432actionViewItemProvider: (action, options) => this.actionViewItemProvider(action as Action, options),433classNames: ['toolbar-toggle-pickReactions', ...ThemeIcon.asClassNameArray(Codicon.reactions)],434anchorAlignmentProvider: () => AnchorAlignment.RIGHT435}436);437}438return this.actionViewItemProvider(action as Action, options);439}440});441442reactions.map(reaction => {443const action = this._reactionActions.add(new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && (reaction.canEdit || hasReactionHandler) ? 'active' : '', (reaction.canEdit || hasReactionHandler), async () => {444try {445await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread, this.comment, reaction);446} catch (e) {447let error: string;448449if (reaction.hasReacted) {450error = e.message451? nls.localize('commentDeleteReactionError', "Deleting the comment reaction failed: {0}.", e.message)452: nls.localize('commentDeleteReactionDefaultError', "Deleting the comment reaction failed");453} else {454error = e.message455? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message)456: nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed");457}458this.notificationService.error(error);459}460}, reaction.reactors, reaction.iconPath, reaction.count));461462this._reactionsActionBar.value?.push(action, { label: true, icon: true });463});464465if (hasReactionHandler) {466const toggleReactionAction = this.createReactionPicker(this.comment.commentReactions || []);467this._reactionsActionBar.value?.push(toggleReactionAction, { label: false, icon: true });468}469}470471get commentBodyValue(): string {472return (typeof this.comment.body === 'string') ? this.comment.body : this.comment.body.value;473}474475private async createCommentEditor(editContainer: HTMLElement): Promise<void> {476this._editModeDisposables.clear();477const container = dom.append(editContainer, dom.$('.edit-textarea'));478this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, container, SimpleCommentEditor.getEditorOptions(this.configurationService), this._contextKeyService, this.parentThread);479this._editModeDisposables.add(this._commentEditor);480481const resource = URI.from({482scheme: Schemas.commentsInput,483path: `/commentinput-${this.comment.uniqueIdInThread}-${Date.now()}.md`484});485const modelRef = await this.textModelService.createModelReference(resource);486this._commentEditorModel = modelRef;487this._editModeDisposables.add(this._commentEditorModel);488489this._commentEditor.setModel(this._commentEditorModel.object.textEditorModel);490this._commentEditor.setValue(this.pendingEdit?.body ?? this.commentBodyValue);491if (this.pendingEdit) {492this._commentEditor.setPosition(this.pendingEdit.cursor);493} else {494const lastLine = this._commentEditorModel.object.textEditorModel.getLineCount();495const lastColumn = this._commentEditorModel.object.textEditorModel.getLineLength(lastLine) + 1;496this._commentEditor.setPosition(new Position(lastLine, lastColumn));497}498this.pendingEdit = undefined;499this._commentEditor.layout({ width: container.clientWidth - 14, height: this._editorHeight });500this._commentEditor.focus();501502dom.scheduleAtNextAnimationFrame(dom.getWindow(editContainer), () => {503this._commentEditor!.layout({ width: container.clientWidth - 14, height: this._editorHeight });504this._commentEditor!.focus();505});506507const commentThread = this.commentThread;508commentThread.input = {509uri: this._commentEditor.getModel()!.uri,510value: this.commentBodyValue511};512this.commentService.setActiveEditingCommentThread(commentThread);513this.commentService.setActiveCommentAndThread(this.owner, { thread: commentThread, comment: this.comment });514515this._editModeDisposables.add(this._commentEditor.onDidFocusEditorWidget(() => {516commentThread.input = {517uri: this._commentEditor!.getModel()!.uri,518value: this.commentBodyValue519};520this.commentService.setActiveEditingCommentThread(commentThread);521this.commentService.setActiveCommentAndThread(this.owner, { thread: commentThread, comment: this.comment });522}));523524this._editModeDisposables.add(this._commentEditor.onDidChangeModelContent(e => {525if (commentThread.input && this._commentEditor && this._commentEditor.getModel()!.uri === commentThread.input.uri) {526const newVal = this._commentEditor.getValue();527if (newVal !== commentThread.input.value) {528const input = commentThread.input;529input.value = newVal;530commentThread.input = input;531this.commentService.setActiveEditingCommentThread(commentThread);532this.commentService.setActiveCommentAndThread(this.owner, { thread: commentThread, comment: this.comment });533}534}535}));536537this.calculateEditorHeight();538539this._editModeDisposables.add((this._commentEditorModel.object.textEditorModel.onDidChangeContent(() => {540if (this._commentEditor && this.calculateEditorHeight()) {541this._commentEditor.layout({ height: this._editorHeight, width: this._commentEditor.getLayoutInfo().width });542this._commentEditor.render(true);543}544})));545546}547548private calculateEditorHeight(): boolean {549if (this._commentEditor) {550const newEditorHeight = calculateEditorHeight(this.parentEditor, this._commentEditor, this._editorHeight);551if (newEditorHeight !== this._editorHeight) {552this._editorHeight = newEditorHeight;553return true;554}555}556return false;557}558559getPendingEdit(): languages.PendingComment | undefined {560const model = this._commentEditor?.getModel();561if (this._commentEditor && model && model.getValueLength() > 0) {562return { body: model.getValue(), cursor: this._commentEditor.getPosition()! };563}564return undefined;565}566567private removeCommentEditor() {568this.isEditing = false;569if (this._editAction) {570this._editAction.enabled = true;571}572this._body.classList.remove('hidden');573this._editModeDisposables.clear();574this._commentEditor = null;575this._commentEditContainer!.remove();576}577578layout(widthInPixel?: number) {579const editorWidth = widthInPixel !== undefined ? widthInPixel - 72 /* - margin and scrollbar*/ : (this._commentEditor?.getLayoutInfo().width ?? 0);580this._commentEditor?.layout({ width: editorWidth, height: this._editorHeight });581const scrollWidth = this._body.scrollWidth;582const width = dom.getContentWidth(this._body);583const scrollHeight = this._body.scrollHeight;584const height = dom.getContentHeight(this._body) + 4;585this._scrollableElement.setScrollDimensions({ width, scrollWidth, height, scrollHeight });586}587588public async switchToEditMode() {589if (this.isEditing) {590return;591}592593this.isEditing = true;594this._body.classList.add('hidden');595this._commentEditContainer = dom.append(this._commentDetailsContainer, dom.$('.edit-container'));596await this.createCommentEditor(this._commentEditContainer);597598const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions'));599const otherActions = dom.append(formActions, dom.$('.other-actions'));600this.createCommentWidgetFormActions(otherActions);601const editorActions = dom.append(formActions, dom.$('.editor-actions'));602this.createCommentWidgetEditorActions(editorActions);603}604605private readonly _editModeDisposables: DisposableStore = this._register(new DisposableStore());606private createCommentWidgetFormActions(container: HTMLElement) {607const menus = this.commentService.getCommentMenus(this.owner);608const menu = menus.getCommentActions(this.comment, this._contextKeyService);609610this._editModeDisposables.add(menu);611this._editModeDisposables.add(menu.onDidChange(() => {612this._commentFormActions?.setActions(menu);613}));614615this._commentFormActions = new CommentFormActions(this.keybindingService, this._contextKeyService, this.contextMenuService, container, (action: IAction): void => {616const text = this._commentEditor!.getValue();617618action.run({619thread: this.commentThread,620commentUniqueId: this.comment.uniqueIdInThread,621text: text,622$mid: MarshalledId.CommentThreadNode623});624625this.removeCommentEditor();626});627628this._editModeDisposables.add(this._commentFormActions);629this._commentFormActions.setActions(menu);630}631632private createCommentWidgetEditorActions(container: HTMLElement) {633const menus = this.commentService.getCommentMenus(this.owner);634const menu = menus.getCommentEditorActions(this._contextKeyService);635636this._editModeDisposables.add(menu);637this._editModeDisposables.add(menu.onDidChange(() => {638this._commentEditorActions?.setActions(menu, true);639}));640641this._commentEditorActions = new CommentFormActions(this.keybindingService, this._contextKeyService, this.contextMenuService, container, (action: IAction): void => {642const text = this._commentEditor!.getValue();643644action.run({645thread: this.commentThread,646commentUniqueId: this.comment.uniqueIdInThread,647text: text,648$mid: MarshalledId.CommentThreadNode649});650651this._commentEditor?.focus();652});653654this._editModeDisposables.add(this._commentEditorActions);655this._commentEditorActions.setActions(menu, true);656}657658setFocus(focused: boolean, visible: boolean = false) {659if (focused) {660this._domNode.focus();661this._actionsToolbarContainer.classList.add('tabfocused');662this._domNode.tabIndex = 0;663if (this.comment.mode === languages.CommentMode.Editing) {664this._commentEditor?.focus();665}666} else {667if (this._actionsToolbarContainer.classList.contains('tabfocused') && !this._actionsToolbarContainer.classList.contains('mouseover')) {668this._domNode.tabIndex = -1;669}670this._actionsToolbarContainer.classList.remove('tabfocused');671}672}673674async update(newComment: languages.Comment) {675676if (newComment.body !== this.comment.body) {677this.updateCommentBody(newComment.body);678}679680if (this.comment.userIconPath && newComment.userIconPath && (URI.from(this.comment.userIconPath).toString() !== URI.from(newComment.userIconPath).toString())) {681this.updateCommentUserIcon(newComment.userIconPath);682}683684const isChangingMode: boolean = newComment.mode !== undefined && newComment.mode !== this.comment.mode;685686this.comment = newComment;687688if (isChangingMode) {689if (newComment.mode === languages.CommentMode.Editing) {690await this.switchToEditMode();691} else {692this.removeCommentEditor();693}694}695696if (newComment.label) {697this._isPendingLabel.innerText = newComment.label;698} else {699this._isPendingLabel.innerText = '';700}701702// update comment reactions703this.createReactionsContainer(this._commentDetailsContainer);704705if (this.comment.contextValue) {706this._commentContextValue.set(this.comment.contextValue);707} else {708this._commentContextValue.reset();709}710711if (this.comment.timestamp) {712this.updateTimestamp(this.comment.timestamp);713}714}715716private onContextMenu(e: MouseEvent) {717const event = new StandardMouseEvent(dom.getWindow(this._domNode), e);718this.contextMenuService.showContextMenu({719getAnchor: () => event,720menuId: MenuId.CommentThreadCommentContext,721menuActionOptions: { shouldForwardArgs: true },722contextKeyService: this._contextKeyService,723actionRunner: this._actionRunner,724getActionsContext: () => {725return this.commentNodeContext;726},727});728}729730focus() {731this.domNode.focus();732if (!this._clearTimeout) {733this.domNode.classList.add('focus');734this._clearTimeout = setTimeout(() => {735this.domNode.classList.remove('focus');736}, 3000);737}738}739740override dispose(): void {741super.dispose();742}743}744745function fillInActions(groups: [string, Array<MenuItemAction | SubmenuItemAction>][], target: IAction[] | { primary: IAction[]; secondary: IAction[] }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void {746for (const tuple of groups) {747let [group, actions] = tuple;748if (useAlternativeActions) {749actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a);750}751752if (isPrimaryGroup(group)) {753const to = Array.isArray(target) ? target : target.primary;754755to.unshift(...actions);756} else {757const to = Array.isArray(target) ? target : target.secondary;758759if (to.length > 0) {760to.push(new Separator());761}762763to.push(...actions);764}765}766}767768769