Path: blob/main/src/vs/workbench/contrib/comments/browser/commentNode.ts
3296 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 { IMarkdownRenderResult, MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';13import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';14import { ICommentService } from './commentService.js';15import { LayoutableEditor, MIN_EDITOR_HEIGHT, SimpleCommentEditor, calculateEditorHeight } from './simpleCommentEditor.js';16import { Emitter, Event } from '../../../../base/common/event.js';17import { INotificationService } from '../../../../platform/notification/common/notification.js';18import { ToolBar } from '../../../../base/browser/ui/toolbar/toolbar.js';19import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';20import { AnchorAlignment } from '../../../../base/browser/ui/contextview/contextview.js';21import { ToggleReactionsAction, ReactionAction, ReactionActionViewItem } from './reactionsAction.js';22import { ICommentThreadWidget } from '../common/commentThreadWidget.js';23import { MenuItemAction, SubmenuItemAction, IMenu, MenuId } from '../../../../platform/actions/common/actions.js';24import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';25import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js';26import { CommentFormActions } from './commentFormActions.js';27import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../base/browser/ui/mouseCursor/mouseCursor.js';28import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';29import { DropdownMenuActionViewItem } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js';30import { Codicon } from '../../../../base/common/codicons.js';31import { ThemeIcon } from '../../../../base/common/themables.js';32import { MarshalledId } from '../../../../base/common/marshallingIds.js';33import { TimestampWidget } from './timestamp.js';34import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';35import { IMarkdownString } from '../../../../base/common/htmlContent.js';36import { IRange } from '../../../../editor/common/core/range.js';37import { ICellRange } from '../../notebook/common/notebookRange.js';38import { CommentMenus } from './commentMenus.js';39import { Scrollable, ScrollbarVisibility } from '../../../../base/common/scrollable.js';40import { SmoothScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';41import { DomEmitter } from '../../../../base/browser/event.js';42import { CommentContextKeys } from '../common/commentContextKeys.js';43import { FileAccess, Schemas } from '../../../../base/common/network.js';44import { COMMENTS_SECTION, ICommentsConfiguration } from '../common/commentsConfiguration.js';45import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';46import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';47import { MarshalledCommentThread } from '../../../common/comments.js';48import { IHoverService } from '../../../../platform/hover/browser/hover.js';49import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js';50import { Position } from '../../../../editor/common/core/position.js';5152class CommentsActionRunner extends ActionRunner {53protected override async runAction(action: IAction, context: any[]): Promise<void> {54await action.run(...context);55}56}5758export class CommentNode<T extends IRange | ICellRange> extends Disposable {59private _domNode: HTMLElement;60private _body: HTMLElement;61private _avatar: HTMLElement;62private readonly _md: MutableDisposable<IMarkdownRenderResult> = this._register(new MutableDisposable());63private _plainText: HTMLElement | undefined;64private _clearTimeout: Timeout | null;6566private _editAction: Action | null = null;67private _commentEditContainer: HTMLElement | null = null;68private _commentDetailsContainer: HTMLElement;69private _actionsToolbarContainer!: HTMLElement;70private readonly _reactionsActionBar: MutableDisposable<ActionBar> = this._register(new MutableDisposable());71private readonly _reactionActions: DisposableStore = this._register(new DisposableStore());72private _reactionActionsContainer?: HTMLElement;73private _commentEditor: SimpleCommentEditor | null = null;74private _commentEditorModel: IReference<IResolvedTextEditorModel> | null = null;75private _editorHeight = MIN_EDITOR_HEIGHT;7677private _isPendingLabel!: HTMLElement;78private _timestamp: HTMLElement | undefined;79private _timestampWidget: TimestampWidget | undefined;80private _contextKeyService: IContextKeyService;81private _commentContextValue: IContextKey<string>;82private _commentMenus: CommentMenus;8384private _scrollable!: Scrollable;85private _scrollableElement!: SmoothScrollableElement;8687private readonly _actionRunner: CommentsActionRunner = this._register(new CommentsActionRunner());88private readonly toolbar: MutableDisposable<ToolBar> = this._register(new MutableDisposable());89private _commentFormActions: CommentFormActions | null = null;90private _commentEditorActions: CommentFormActions | null = null;9192private readonly _onDidClick = new Emitter<CommentNode<T>>();9394public get domNode(): HTMLElement {95return this._domNode;96}9798public isEditing: boolean = false;99100constructor(101private readonly parentEditor: LayoutableEditor,102private commentThread: languages.CommentThread<T>,103public comment: languages.Comment,104private pendingEdit: languages.PendingComment | undefined,105private owner: string,106private resource: URI,107private parentThread: ICommentThreadWidget,108private markdownRenderer: MarkdownRenderer,109@IInstantiationService private instantiationService: IInstantiationService,110@ICommentService private commentService: ICommentService,111@INotificationService private notificationService: INotificationService,112@IContextMenuService private contextMenuService: IContextMenuService,113@IContextKeyService contextKeyService: IContextKeyService,114@IConfigurationService private configurationService: IConfigurationService,115@IHoverService private hoverService: IHoverService,116@IKeybindingService private keybindingService: IKeybindingService,117@ITextModelService private readonly textModelService: ITextModelService,118) {119super();120121this._domNode = dom.$('div.review-comment');122this._contextKeyService = this._register(contextKeyService.createScoped(this._domNode));123this._commentContextValue = CommentContextKeys.commentContext.bindTo(this._contextKeyService);124if (this.comment.contextValue) {125this._commentContextValue.set(this.comment.contextValue);126}127this._commentMenus = this.commentService.getCommentMenus(this.owner);128129this._domNode.tabIndex = -1;130this._avatar = dom.append(this._domNode, dom.$('div.avatar-container'));131this.updateCommentUserIcon(this.comment.userIconPath);132133this._commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents'));134135this.createHeader(this._commentDetailsContainer);136this._body = document.createElement(`div`);137this._body.classList.add('comment-body', MOUSE_CURSOR_TEXT_CSS_CLASS_NAME);138if (configurationService.getValue<ICommentsConfiguration | undefined>(COMMENTS_SECTION)?.maxHeight !== false) {139this._body.classList.add('comment-body-max-height');140}141142this.createScroll(this._commentDetailsContainer, this._body);143this.updateCommentBody(this.comment.body);144145this.createReactionsContainer(this._commentDetailsContainer);146147this._domNode.setAttribute('aria-label', `${comment.userName}, ${this.commentBodyValue}`);148this._domNode.setAttribute('role', 'treeitem');149this._clearTimeout = null;150151this._register(dom.addDisposableListener(this._domNode, dom.EventType.CLICK, () => this.isEditing || this._onDidClick.fire(this)));152this._register(dom.addDisposableListener(this._domNode, dom.EventType.CONTEXT_MENU, e => {153return this.onContextMenu(e);154}));155156if (pendingEdit) {157this.switchToEditMode();158}159160this.activeCommentListeners();161}162163private activeCommentListeners() {164this._register(dom.addDisposableListener(this._domNode, dom.EventType.FOCUS_IN, () => {165this.commentService.setActiveCommentAndThread(this.owner, { thread: this.commentThread, comment: this.comment });166}, true));167}168169private createScroll(container: HTMLElement, body: HTMLElement) {170this._scrollable = this._register(new Scrollable({171forceIntegerValues: true,172smoothScrollDuration: 125,173scheduleAtNextAnimationFrame: cb => dom.scheduleAtNextAnimationFrame(dom.getWindow(container), cb)174}));175this._scrollableElement = this._register(new SmoothScrollableElement(body, {176horizontal: ScrollbarVisibility.Visible,177vertical: ScrollbarVisibility.Visible178}, this._scrollable));179180this._register(this._scrollableElement.onScroll(e => {181if (e.scrollLeftChanged) {182body.scrollLeft = e.scrollLeft;183}184if (e.scrollTopChanged) {185body.scrollTop = e.scrollTop;186}187}));188189const onDidScrollViewContainer = this._register(new DomEmitter(body, 'scroll')).event;190this._register(onDidScrollViewContainer(_ => {191const position = this._scrollableElement.getScrollPosition();192const scrollLeft = Math.abs(body.scrollLeft - position.scrollLeft) <= 1 ? undefined : body.scrollLeft;193const scrollTop = Math.abs(body.scrollTop - position.scrollTop) <= 1 ? undefined : body.scrollTop;194195if (scrollLeft !== undefined || scrollTop !== undefined) {196this._scrollableElement.setScrollPosition({ scrollLeft, scrollTop });197}198}));199200container.appendChild(this._scrollableElement.getDomNode());201}202203private updateCommentBody(body: string | IMarkdownString) {204this._body.innerText = '';205this._md.clear();206this._plainText = undefined;207if (typeof body === 'string') {208this._plainText = dom.append(this._body, dom.$('.comment-body-plainstring'));209this._plainText.innerText = body;210} else {211this._md.value = this.markdownRenderer.render(body);212this._body.appendChild(this._md.value.element);213}214}215216private updateCommentUserIcon(userIconPath: UriComponents | undefined) {217this._avatar.textContent = '';218if (userIconPath) {219const img = dom.append(this._avatar, dom.$('img.avatar')) as HTMLImageElement;220img.src = FileAccess.uriToBrowserUri(URI.revive(userIconPath)).toString(true);221img.onerror = _ => img.remove();222}223}224225public get onDidClick(): Event<CommentNode<T>> {226return this._onDidClick.event;227}228229private createTimestamp(container: HTMLElement) {230this._timestamp = dom.append(container, dom.$('span.timestamp-container'));231this.updateTimestamp(this.comment.timestamp);232}233234private updateTimestamp(raw?: string) {235if (!this._timestamp) {236return;237}238239const timestamp = raw !== undefined ? new Date(raw) : undefined;240if (!timestamp) {241this._timestampWidget?.dispose();242} else {243if (!this._timestampWidget) {244this._timestampWidget = new TimestampWidget(this.configurationService, this.hoverService, this._timestamp, timestamp);245this._register(this._timestampWidget);246} else {247this._timestampWidget.setTimestamp(timestamp);248}249}250}251252private createHeader(commentDetailsContainer: HTMLElement): void {253const header = dom.append(commentDetailsContainer, dom.$(`div.comment-title.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`));254const infoContainer = dom.append(header, dom.$('comment-header-info'));255const author = dom.append(infoContainer, dom.$('strong.author'));256author.innerText = this.comment.userName;257this.createTimestamp(infoContainer);258this._isPendingLabel = dom.append(infoContainer, dom.$('span.isPending'));259260if (this.comment.label) {261this._isPendingLabel.innerText = this.comment.label;262} else {263this._isPendingLabel.innerText = '';264}265266this._actionsToolbarContainer = dom.append(header, dom.$('.comment-actions'));267this.createActionsToolbar();268}269270private getToolbarActions(menu: IMenu): { primary: IAction[]; secondary: IAction[] } {271const contributedActions = menu.getActions({ shouldForwardArgs: true });272const primary: IAction[] = [];273const secondary: IAction[] = [];274const result = { primary, secondary };275fillInActions(contributedActions, result, false, g => /^inline/.test(g));276return result;277}278279private get commentNodeContext(): [any, MarshalledCommentThread] {280return [{281thread: this.commentThread,282commentUniqueId: this.comment.uniqueIdInThread,283$mid: MarshalledId.CommentNode284},285{286commentControlHandle: this.commentThread.controllerHandle,287commentThreadHandle: this.commentThread.commentThreadHandle,288$mid: MarshalledId.CommentThread289}];290}291292private createToolbar() {293this.toolbar.value = new ToolBar(this._actionsToolbarContainer, this.contextMenuService, {294actionViewItemProvider: (action, options) => {295if (action.id === ToggleReactionsAction.ID) {296return new DropdownMenuActionViewItem(297action,298(<ToggleReactionsAction>action).menuActions,299this.contextMenuService,300{301...options,302actionViewItemProvider: (action, options) => this.actionViewItemProvider(action as Action, options),303classNames: ['toolbar-toggle-pickReactions', ...ThemeIcon.asClassNameArray(Codicon.reactions)],304anchorAlignmentProvider: () => AnchorAlignment.RIGHT305}306);307}308return this.actionViewItemProvider(action as Action, options);309},310orientation: ActionsOrientation.HORIZONTAL311});312313this.toolbar.value.context = this.commentNodeContext;314this.toolbar.value.actionRunner = this._actionRunner;315}316317private createActionsToolbar() {318const actions: IAction[] = [];319320const menu = this._commentMenus.getCommentTitleActions(this.comment, this._contextKeyService);321this._register(menu);322this._register(menu.onDidChange(e => {323const { primary, secondary } = this.getToolbarActions(menu);324if (!this.toolbar && (primary.length || secondary.length)) {325this.createToolbar();326}327this.toolbar.value!.setActions(primary, secondary);328}));329330const { primary, secondary } = this.getToolbarActions(menu);331actions.push(...primary);332333if (actions.length || secondary.length) {334this.createToolbar();335this.toolbar.value!.setActions(actions, secondary);336}337}338339actionViewItemProvider(action: Action, options: IActionViewItemOptions) {340if (action.id === ToggleReactionsAction.ID) {341options = { label: false, icon: true };342} else {343options = { label: false, icon: true };344}345346if (action.id === ReactionAction.ID) {347const item = new ReactionActionViewItem(action);348return item;349} else if (action instanceof MenuItemAction) {350return this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate });351} else if (action instanceof SubmenuItemAction) {352return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, options);353} else {354const item = new ActionViewItem({}, action, options);355return item;356}357}358359async submitComment(): Promise<void> {360if (this._commentEditor && this._commentFormActions) {361await this._commentFormActions.triggerDefaultAction();362this.pendingEdit = undefined;363}364}365366private createReactionPicker(reactionGroup: languages.CommentReaction[]): ToggleReactionsAction {367const toggleReactionAction = this._reactionActions.add(new ToggleReactionsAction(() => {368toggleReactionActionViewItem?.show();369}, nls.localize('commentToggleReaction', "Toggle Reaction")));370371let reactionMenuActions: Action[] = [];372if (reactionGroup && reactionGroup.length) {373reactionMenuActions = reactionGroup.map((reaction) => {374return this._reactionActions.add(new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => {375try {376await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread, this.comment, reaction);377} catch (e) {378const error = e.message379? nls.localize('commentToggleReactionError', "Toggling the comment reaction failed: {0}.", e.message)380: nls.localize('commentToggleReactionDefaultError', "Toggling the comment reaction failed");381this.notificationService.error(error);382}383}));384});385}386387toggleReactionAction.menuActions = reactionMenuActions;388389const toggleReactionActionViewItem: DropdownMenuActionViewItem = this._reactionActions.add(new DropdownMenuActionViewItem(390toggleReactionAction,391(<ToggleReactionsAction>toggleReactionAction).menuActions,392this.contextMenuService,393{394actionViewItemProvider: (action, options) => {395if (action.id === ToggleReactionsAction.ID) {396return toggleReactionActionViewItem;397}398return this.actionViewItemProvider(action as Action, options);399},400classNames: 'toolbar-toggle-pickReactions',401anchorAlignmentProvider: () => AnchorAlignment.RIGHT402}403));404405return toggleReactionAction;406}407408private createReactionsContainer(commentDetailsContainer: HTMLElement): void {409this._reactionActionsContainer?.remove();410this._reactionsActionBar.clear();411this._reactionActions.clear();412413this._reactionActionsContainer = dom.append(commentDetailsContainer, dom.$('div.comment-reactions'));414this._reactionsActionBar.value = new ActionBar(this._reactionActionsContainer, {415actionViewItemProvider: (action, options) => {416if (action.id === ToggleReactionsAction.ID) {417return new DropdownMenuActionViewItem(418action,419(<ToggleReactionsAction>action).menuActions,420this.contextMenuService,421{422actionViewItemProvider: (action, options) => this.actionViewItemProvider(action as Action, options),423classNames: ['toolbar-toggle-pickReactions', ...ThemeIcon.asClassNameArray(Codicon.reactions)],424anchorAlignmentProvider: () => AnchorAlignment.RIGHT425}426);427}428return this.actionViewItemProvider(action as Action, options);429}430});431432const hasReactionHandler = this.commentService.hasReactionHandler(this.owner);433this.comment.commentReactions?.filter(reaction => !!reaction.count).map(reaction => {434const action = this._reactionActions.add(new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && (reaction.canEdit || hasReactionHandler) ? 'active' : '', (reaction.canEdit || hasReactionHandler), async () => {435try {436await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread, this.comment, reaction);437} catch (e) {438let error: string;439440if (reaction.hasReacted) {441error = e.message442? nls.localize('commentDeleteReactionError', "Deleting the comment reaction failed: {0}.", e.message)443: nls.localize('commentDeleteReactionDefaultError', "Deleting the comment reaction failed");444} else {445error = e.message446? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message)447: nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed");448}449this.notificationService.error(error);450}451}, reaction.reactors, reaction.iconPath, reaction.count));452453this._reactionsActionBar.value?.push(action, { label: true, icon: true });454});455456if (hasReactionHandler) {457const toggleReactionAction = this.createReactionPicker(this.comment.commentReactions || []);458this._reactionsActionBar.value?.push(toggleReactionAction, { label: false, icon: true });459}460}461462get commentBodyValue(): string {463return (typeof this.comment.body === 'string') ? this.comment.body : this.comment.body.value;464}465466private async createCommentEditor(editContainer: HTMLElement): Promise<void> {467this._editModeDisposables.clear();468const container = dom.append(editContainer, dom.$('.edit-textarea'));469this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, container, SimpleCommentEditor.getEditorOptions(this.configurationService), this._contextKeyService, this.parentThread);470this._editModeDisposables.add(this._commentEditor);471472const resource = URI.from({473scheme: Schemas.commentsInput,474path: `/commentinput-${this.comment.uniqueIdInThread}-${Date.now()}.md`475});476const modelRef = await this.textModelService.createModelReference(resource);477this._commentEditorModel = modelRef;478this._editModeDisposables.add(this._commentEditorModel);479480this._commentEditor.setModel(this._commentEditorModel.object.textEditorModel);481this._commentEditor.setValue(this.pendingEdit?.body ?? this.commentBodyValue);482if (this.pendingEdit) {483this._commentEditor.setPosition(this.pendingEdit.cursor);484} else {485const lastLine = this._commentEditorModel.object.textEditorModel.getLineCount();486const lastColumn = this._commentEditorModel.object.textEditorModel.getLineLength(lastLine) + 1;487this._commentEditor.setPosition(new Position(lastLine, lastColumn));488}489this.pendingEdit = undefined;490this._commentEditor.layout({ width: container.clientWidth - 14, height: this._editorHeight });491this._commentEditor.focus();492493dom.scheduleAtNextAnimationFrame(dom.getWindow(editContainer), () => {494this._commentEditor!.layout({ width: container.clientWidth - 14, height: this._editorHeight });495this._commentEditor!.focus();496});497498const commentThread = this.commentThread;499commentThread.input = {500uri: this._commentEditor.getModel()!.uri,501value: this.commentBodyValue502};503this.commentService.setActiveEditingCommentThread(commentThread);504this.commentService.setActiveCommentAndThread(this.owner, { thread: commentThread, comment: this.comment });505506this._editModeDisposables.add(this._commentEditor.onDidFocusEditorWidget(() => {507commentThread.input = {508uri: this._commentEditor!.getModel()!.uri,509value: this.commentBodyValue510};511this.commentService.setActiveEditingCommentThread(commentThread);512this.commentService.setActiveCommentAndThread(this.owner, { thread: commentThread, comment: this.comment });513}));514515this._editModeDisposables.add(this._commentEditor.onDidChangeModelContent(e => {516if (commentThread.input && this._commentEditor && this._commentEditor.getModel()!.uri === commentThread.input.uri) {517const newVal = this._commentEditor.getValue();518if (newVal !== commentThread.input.value) {519const input = commentThread.input;520input.value = newVal;521commentThread.input = input;522this.commentService.setActiveEditingCommentThread(commentThread);523this.commentService.setActiveCommentAndThread(this.owner, { thread: commentThread, comment: this.comment });524}525}526}));527528this.calculateEditorHeight();529530this._editModeDisposables.add((this._commentEditorModel.object.textEditorModel.onDidChangeContent(() => {531if (this._commentEditor && this.calculateEditorHeight()) {532this._commentEditor.layout({ height: this._editorHeight, width: this._commentEditor.getLayoutInfo().width });533this._commentEditor.render(true);534}535})));536537}538539private calculateEditorHeight(): boolean {540if (this._commentEditor) {541const newEditorHeight = calculateEditorHeight(this.parentEditor, this._commentEditor, this._editorHeight);542if (newEditorHeight !== this._editorHeight) {543this._editorHeight = newEditorHeight;544return true;545}546}547return false;548}549550getPendingEdit(): languages.PendingComment | undefined {551const model = this._commentEditor?.getModel();552if (this._commentEditor && model && model.getValueLength() > 0) {553return { body: model.getValue(), cursor: this._commentEditor.getPosition()! };554}555return undefined;556}557558private removeCommentEditor() {559this.isEditing = false;560if (this._editAction) {561this._editAction.enabled = true;562}563this._body.classList.remove('hidden');564this._editModeDisposables.clear();565this._commentEditor = null;566this._commentEditContainer!.remove();567}568569layout(widthInPixel?: number) {570const editorWidth = widthInPixel !== undefined ? widthInPixel - 72 /* - margin and scrollbar*/ : (this._commentEditor?.getLayoutInfo().width ?? 0);571this._commentEditor?.layout({ width: editorWidth, height: this._editorHeight });572const scrollWidth = this._body.scrollWidth;573const width = dom.getContentWidth(this._body);574const scrollHeight = this._body.scrollHeight;575const height = dom.getContentHeight(this._body) + 4;576this._scrollableElement.setScrollDimensions({ width, scrollWidth, height, scrollHeight });577}578579public async switchToEditMode() {580if (this.isEditing) {581return;582}583584this.isEditing = true;585this._body.classList.add('hidden');586this._commentEditContainer = dom.append(this._commentDetailsContainer, dom.$('.edit-container'));587await this.createCommentEditor(this._commentEditContainer);588589const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions'));590const otherActions = dom.append(formActions, dom.$('.other-actions'));591this.createCommentWidgetFormActions(otherActions);592const editorActions = dom.append(formActions, dom.$('.editor-actions'));593this.createCommentWidgetEditorActions(editorActions);594}595596private readonly _editModeDisposables: DisposableStore = this._register(new DisposableStore());597private createCommentWidgetFormActions(container: HTMLElement) {598const menus = this.commentService.getCommentMenus(this.owner);599const menu = menus.getCommentActions(this.comment, this._contextKeyService);600601this._editModeDisposables.add(menu);602this._editModeDisposables.add(menu.onDidChange(() => {603this._commentFormActions?.setActions(menu);604}));605606this._commentFormActions = new CommentFormActions(this.keybindingService, this._contextKeyService, this.contextMenuService, container, (action: IAction): void => {607const text = this._commentEditor!.getValue();608609action.run({610thread: this.commentThread,611commentUniqueId: this.comment.uniqueIdInThread,612text: text,613$mid: MarshalledId.CommentThreadNode614});615616this.removeCommentEditor();617});618619this._editModeDisposables.add(this._commentFormActions);620this._commentFormActions.setActions(menu);621}622623private createCommentWidgetEditorActions(container: HTMLElement) {624const menus = this.commentService.getCommentMenus(this.owner);625const menu = menus.getCommentEditorActions(this._contextKeyService);626627this._editModeDisposables.add(menu);628this._editModeDisposables.add(menu.onDidChange(() => {629this._commentEditorActions?.setActions(menu, true);630}));631632this._commentEditorActions = new CommentFormActions(this.keybindingService, this._contextKeyService, this.contextMenuService, container, (action: IAction): void => {633const text = this._commentEditor!.getValue();634635action.run({636thread: this.commentThread,637commentUniqueId: this.comment.uniqueIdInThread,638text: text,639$mid: MarshalledId.CommentThreadNode640});641642this._commentEditor?.focus();643});644645this._editModeDisposables.add(this._commentEditorActions);646this._commentEditorActions.setActions(menu, true);647}648649setFocus(focused: boolean, visible: boolean = false) {650if (focused) {651this._domNode.focus();652this._actionsToolbarContainer.classList.add('tabfocused');653this._domNode.tabIndex = 0;654if (this.comment.mode === languages.CommentMode.Editing) {655this._commentEditor?.focus();656}657} else {658if (this._actionsToolbarContainer.classList.contains('tabfocused') && !this._actionsToolbarContainer.classList.contains('mouseover')) {659this._domNode.tabIndex = -1;660}661this._actionsToolbarContainer.classList.remove('tabfocused');662}663}664665async update(newComment: languages.Comment) {666667if (newComment.body !== this.comment.body) {668this.updateCommentBody(newComment.body);669}670671if (this.comment.userIconPath && newComment.userIconPath && (URI.from(this.comment.userIconPath).toString() !== URI.from(newComment.userIconPath).toString())) {672this.updateCommentUserIcon(newComment.userIconPath);673}674675const isChangingMode: boolean = newComment.mode !== undefined && newComment.mode !== this.comment.mode;676677this.comment = newComment;678679if (isChangingMode) {680if (newComment.mode === languages.CommentMode.Editing) {681await this.switchToEditMode();682} else {683this.removeCommentEditor();684}685}686687if (newComment.label) {688this._isPendingLabel.innerText = newComment.label;689} else {690this._isPendingLabel.innerText = '';691}692693// update comment reactions694this.createReactionsContainer(this._commentDetailsContainer);695696if (this.comment.contextValue) {697this._commentContextValue.set(this.comment.contextValue);698} else {699this._commentContextValue.reset();700}701702if (this.comment.timestamp) {703this.updateTimestamp(this.comment.timestamp);704}705}706707private onContextMenu(e: MouseEvent) {708const event = new StandardMouseEvent(dom.getWindow(this._domNode), e);709this.contextMenuService.showContextMenu({710getAnchor: () => event,711menuId: MenuId.CommentThreadCommentContext,712menuActionOptions: { shouldForwardArgs: true },713contextKeyService: this._contextKeyService,714actionRunner: this._actionRunner,715getActionsContext: () => {716return this.commentNodeContext;717},718});719}720721focus() {722this.domNode.focus();723if (!this._clearTimeout) {724this.domNode.classList.add('focus');725this._clearTimeout = setTimeout(() => {726this.domNode.classList.remove('focus');727}, 3000);728}729}730731override dispose(): void {732super.dispose();733}734}735736function fillInActions(groups: [string, Array<MenuItemAction | SubmenuItemAction>][], target: IAction[] | { primary: IAction[]; secondary: IAction[] }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void {737for (const tuple of groups) {738let [group, actions] = tuple;739if (useAlternativeActions) {740actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a);741}742743if (isPrimaryGroup(group)) {744const to = Array.isArray(target) ? target : target.primary;745746to.unshift(...actions);747} else {748const to = Array.isArray(target) ? target : target.secondary;749750if (to.length > 0) {751to.push(new Separator());752}753754to.push(...actions);755}756}757}758759760