Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/comments/browser/commentNode.ts
5237 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as nls from '../../../../nls.js';
7
import * as dom from '../../../../base/browser/dom.js';
8
import * as languages from '../../../../editor/common/languages.js';
9
import { ActionsOrientation, ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
10
import { Action, IAction, Separator, ActionRunner } from '../../../../base/common/actions.js';
11
import { Disposable, DisposableStore, IReference, MutableDisposable } from '../../../../base/common/lifecycle.js';
12
import { URI, UriComponents } from '../../../../base/common/uri.js';
13
import { IMarkdownRendererExtraOptions, IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
14
import { IRenderedMarkdown } from '../../../../base/browser/markdownRenderer.js';
15
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
16
import { ICommentService } from './commentService.js';
17
import { LayoutableEditor, MIN_EDITOR_HEIGHT, SimpleCommentEditor, calculateEditorHeight } from './simpleCommentEditor.js';
18
import { Emitter, Event } from '../../../../base/common/event.js';
19
import { INotificationService } from '../../../../platform/notification/common/notification.js';
20
import { ToolBar } from '../../../../base/browser/ui/toolbar/toolbar.js';
21
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
22
import { AnchorAlignment } from '../../../../base/browser/ui/contextview/contextview.js';
23
import { ToggleReactionsAction, ReactionAction, ReactionActionViewItem } from './reactionsAction.js';
24
import { ICommentThreadWidget } from '../common/commentThreadWidget.js';
25
import { MenuItemAction, SubmenuItemAction, IMenu, MenuId } from '../../../../platform/actions/common/actions.js';
26
import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
27
import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js';
28
import { CommentFormActions } from './commentFormActions.js';
29
import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../base/browser/ui/mouseCursor/mouseCursor.js';
30
import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
31
import { DropdownMenuActionViewItem } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js';
32
import { Codicon } from '../../../../base/common/codicons.js';
33
import { ThemeIcon } from '../../../../base/common/themables.js';
34
import { MarshalledId } from '../../../../base/common/marshallingIds.js';
35
import { TimestampWidget } from './timestamp.js';
36
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
37
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
38
import { IRange } from '../../../../editor/common/core/range.js';
39
import { ICellRange } from '../../notebook/common/notebookRange.js';
40
import { CommentMenus } from './commentMenus.js';
41
import { Scrollable, ScrollbarVisibility } from '../../../../base/common/scrollable.js';
42
import { SmoothScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
43
import { DomEmitter } from '../../../../base/browser/event.js';
44
import { CommentContextKeys } from '../common/commentContextKeys.js';
45
import { FileAccess, Schemas } from '../../../../base/common/network.js';
46
import { COMMENTS_SECTION, ICommentsConfiguration } from '../common/commentsConfiguration.js';
47
import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
48
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
49
import { MarshalledCommentThread } from '../../../common/comments.js';
50
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
51
import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js';
52
import { Position } from '../../../../editor/common/core/position.js';
53
54
class CommentsActionRunner extends ActionRunner {
55
protected override async runAction(action: IAction, context: unknown[]): Promise<void> {
56
await action.run(...context);
57
}
58
}
59
60
export class CommentNode<T extends IRange | ICellRange> extends Disposable {
61
private _domNode: HTMLElement;
62
private _body: HTMLElement;
63
private _avatar: HTMLElement;
64
private readonly _md: MutableDisposable<IRenderedMarkdown> = this._register(new MutableDisposable());
65
private _plainText: HTMLElement | undefined;
66
private _clearTimeout: Timeout | null;
67
68
private _editAction: Action | null = null;
69
private _commentEditContainer: HTMLElement | null = null;
70
private _commentDetailsContainer: HTMLElement;
71
private _actionsToolbarContainer!: HTMLElement;
72
private readonly _reactionsActionBar: MutableDisposable<ActionBar> = this._register(new MutableDisposable());
73
private readonly _reactionActions: DisposableStore = this._register(new DisposableStore());
74
private _reactionActionsContainer?: HTMLElement;
75
private _commentEditor: SimpleCommentEditor | null = null;
76
private _commentEditorModel: IReference<IResolvedTextEditorModel> | null = null;
77
private _editorHeight = MIN_EDITOR_HEIGHT;
78
79
private _isPendingLabel!: HTMLElement;
80
private _timestamp: HTMLElement | undefined;
81
private _timestampWidget: TimestampWidget | undefined;
82
private _contextKeyService: IContextKeyService;
83
private _commentContextValue: IContextKey<string>;
84
private _commentMenus: CommentMenus;
85
86
private _scrollable!: Scrollable;
87
private _scrollableElement!: SmoothScrollableElement;
88
89
private readonly _actionRunner: CommentsActionRunner = this._register(new CommentsActionRunner());
90
private readonly toolbar: MutableDisposable<ToolBar> = this._register(new MutableDisposable());
91
private _commentFormActions: CommentFormActions | null = null;
92
private _commentEditorActions: CommentFormActions | null = null;
93
94
private readonly _onDidClick = new Emitter<CommentNode<T>>();
95
96
public get domNode(): HTMLElement {
97
return this._domNode;
98
}
99
100
public isEditing: boolean = false;
101
102
constructor(
103
private readonly parentEditor: LayoutableEditor,
104
private commentThread: languages.CommentThread<T>,
105
public comment: languages.Comment,
106
private pendingEdit: languages.PendingComment | undefined,
107
private owner: string,
108
private resource: URI,
109
private parentThread: ICommentThreadWidget,
110
private readonly markdownRendererOptions: IMarkdownRendererExtraOptions,
111
@IInstantiationService private instantiationService: IInstantiationService,
112
@ICommentService private commentService: ICommentService,
113
@INotificationService private notificationService: INotificationService,
114
@IContextMenuService private contextMenuService: IContextMenuService,
115
@IContextKeyService contextKeyService: IContextKeyService,
116
@IConfigurationService private configurationService: IConfigurationService,
117
@IHoverService private hoverService: IHoverService,
118
@IKeybindingService private keybindingService: IKeybindingService,
119
@ITextModelService private readonly textModelService: ITextModelService,
120
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
121
) {
122
super();
123
124
this._domNode = dom.$('div.review-comment');
125
this._contextKeyService = this._register(contextKeyService.createScoped(this._domNode));
126
this._commentContextValue = CommentContextKeys.commentContext.bindTo(this._contextKeyService);
127
if (this.comment.contextValue) {
128
this._commentContextValue.set(this.comment.contextValue);
129
}
130
this._commentMenus = this.commentService.getCommentMenus(this.owner);
131
132
this._domNode.tabIndex = -1;
133
this._avatar = dom.append(this._domNode, dom.$('div.avatar-container'));
134
this.updateCommentUserIcon(this.comment.userIconPath);
135
136
this._commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents'));
137
138
this.createHeader(this._commentDetailsContainer);
139
this._body = document.createElement(`div`);
140
this._body.classList.add('comment-body', MOUSE_CURSOR_TEXT_CSS_CLASS_NAME);
141
if (configurationService.getValue<ICommentsConfiguration | undefined>(COMMENTS_SECTION)?.maxHeight !== false) {
142
this._body.classList.add('comment-body-max-height');
143
}
144
145
this.createScroll(this._commentDetailsContainer, this._body);
146
this.updateCommentBody(this.comment.body);
147
148
this.createReactionsContainer(this._commentDetailsContainer);
149
150
this._domNode.setAttribute('aria-label', `${comment.userName}, ${this.commentBodyValue}`);
151
this._domNode.setAttribute('role', 'treeitem');
152
this._clearTimeout = null;
153
154
this._register(dom.addDisposableListener(this._domNode, dom.EventType.CLICK, () => this.isEditing || this._onDidClick.fire(this)));
155
this._register(dom.addDisposableListener(this._domNode, dom.EventType.CONTEXT_MENU, e => {
156
return this.onContextMenu(e);
157
}));
158
159
if (pendingEdit) {
160
this.switchToEditMode();
161
}
162
163
this.activeCommentListeners();
164
}
165
166
private activeCommentListeners() {
167
this._register(dom.addDisposableListener(this._domNode, dom.EventType.FOCUS_IN, () => {
168
this.commentService.setActiveCommentAndThread(this.owner, { thread: this.commentThread, comment: this.comment });
169
}, true));
170
}
171
172
private createScroll(container: HTMLElement, body: HTMLElement) {
173
this._scrollable = this._register(new Scrollable({
174
forceIntegerValues: true,
175
smoothScrollDuration: 125,
176
scheduleAtNextAnimationFrame: cb => dom.scheduleAtNextAnimationFrame(dom.getWindow(container), cb)
177
}));
178
this._scrollableElement = this._register(new SmoothScrollableElement(body, {
179
horizontal: ScrollbarVisibility.Visible,
180
vertical: ScrollbarVisibility.Visible
181
}, this._scrollable));
182
183
this._register(this._scrollableElement.onScroll(e => {
184
if (e.scrollLeftChanged) {
185
body.scrollLeft = e.scrollLeft;
186
}
187
if (e.scrollTopChanged) {
188
body.scrollTop = e.scrollTop;
189
}
190
}));
191
192
const onDidScrollViewContainer = this._register(new DomEmitter(body, 'scroll')).event;
193
this._register(onDidScrollViewContainer(_ => {
194
const position = this._scrollableElement.getScrollPosition();
195
const scrollLeft = Math.abs(body.scrollLeft - position.scrollLeft) <= 1 ? undefined : body.scrollLeft;
196
const scrollTop = Math.abs(body.scrollTop - position.scrollTop) <= 1 ? undefined : body.scrollTop;
197
198
if (scrollLeft !== undefined || scrollTop !== undefined) {
199
this._scrollableElement.setScrollPosition({ scrollLeft, scrollTop });
200
}
201
}));
202
203
container.appendChild(this._scrollableElement.getDomNode());
204
}
205
206
private updateCommentBody(body: string | IMarkdownString) {
207
this._body.innerText = '';
208
this._md.clear();
209
this._plainText = undefined;
210
if (typeof body === 'string') {
211
this._plainText = dom.append(this._body, dom.$('.comment-body-plainstring'));
212
this._plainText.innerText = body;
213
} else {
214
this._md.value = this.markdownRendererService.render(body, this.markdownRendererOptions);
215
this._body.appendChild(this._md.value.element);
216
}
217
}
218
219
private updateCommentUserIcon(userIconPath: UriComponents | undefined) {
220
this._avatar.textContent = '';
221
if (userIconPath) {
222
const img = dom.append(this._avatar, dom.$('img.avatar')) as HTMLImageElement;
223
img.src = FileAccess.uriToBrowserUri(URI.revive(userIconPath)).toString(true);
224
img.onerror = _ => img.remove();
225
}
226
}
227
228
public get onDidClick(): Event<CommentNode<T>> {
229
return this._onDidClick.event;
230
}
231
232
private createTimestamp(container: HTMLElement) {
233
this._timestamp = dom.append(container, dom.$('span.timestamp-container'));
234
this.updateTimestamp(this.comment.timestamp);
235
}
236
237
private updateTimestamp(raw?: string) {
238
if (!this._timestamp) {
239
return;
240
}
241
242
const timestamp = raw !== undefined ? new Date(raw) : undefined;
243
if (!timestamp) {
244
this._timestampWidget?.dispose();
245
} else {
246
if (!this._timestampWidget) {
247
this._timestampWidget = new TimestampWidget(this.configurationService, this.hoverService, this._timestamp, timestamp);
248
this._register(this._timestampWidget);
249
} else {
250
this._timestampWidget.setTimestamp(timestamp);
251
}
252
}
253
}
254
255
private createHeader(commentDetailsContainer: HTMLElement): void {
256
const header = dom.append(commentDetailsContainer, dom.$(`div.comment-title.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`));
257
const infoContainer = dom.append(header, dom.$('comment-header-info'));
258
const author = dom.append(infoContainer, dom.$('strong.author'));
259
author.innerText = this.comment.userName;
260
this.createTimestamp(infoContainer);
261
this._isPendingLabel = dom.append(infoContainer, dom.$('span.isPending'));
262
263
if (this.comment.label) {
264
this._isPendingLabel.innerText = this.comment.label;
265
} else {
266
this._isPendingLabel.innerText = '';
267
}
268
269
this._actionsToolbarContainer = dom.append(header, dom.$('.comment-actions'));
270
this.createActionsToolbar();
271
}
272
273
private getToolbarActions(menu: IMenu): { primary: IAction[]; secondary: IAction[] } {
274
const contributedActions = menu.getActions({ shouldForwardArgs: true });
275
const primary: IAction[] = [];
276
const secondary: IAction[] = [];
277
const result = { primary, secondary };
278
fillInActions(contributedActions, result, false, g => /^inline/.test(g));
279
return result;
280
}
281
282
private get commentNodeContext(): [{ thread: languages.CommentThread<T>; commentUniqueId: number; $mid: MarshalledId.CommentNode }, MarshalledCommentThread] {
283
return [{
284
thread: this.commentThread,
285
commentUniqueId: this.comment.uniqueIdInThread,
286
$mid: MarshalledId.CommentNode
287
},
288
{
289
commentControlHandle: this.commentThread.controllerHandle,
290
commentThreadHandle: this.commentThread.commentThreadHandle,
291
$mid: MarshalledId.CommentThread
292
}];
293
}
294
295
private createToolbar() {
296
this.toolbar.value = new ToolBar(this._actionsToolbarContainer, this.contextMenuService, {
297
actionViewItemProvider: (action, options) => {
298
if (action.id === ToggleReactionsAction.ID) {
299
return new DropdownMenuActionViewItem(
300
action,
301
(<ToggleReactionsAction>action).menuActions,
302
this.contextMenuService,
303
{
304
...options,
305
actionViewItemProvider: (action, options) => this.actionViewItemProvider(action as Action, options),
306
classNames: ['toolbar-toggle-pickReactions', ...ThemeIcon.asClassNameArray(Codicon.reactions)],
307
anchorAlignmentProvider: () => AnchorAlignment.RIGHT
308
}
309
);
310
}
311
return this.actionViewItemProvider(action as Action, options);
312
},
313
orientation: ActionsOrientation.HORIZONTAL
314
});
315
316
this.toolbar.value.context = this.commentNodeContext;
317
this.toolbar.value.actionRunner = this._actionRunner;
318
}
319
320
private createActionsToolbar() {
321
const actions: IAction[] = [];
322
323
const menu = this._commentMenus.getCommentTitleActions(this.comment, this._contextKeyService);
324
this._register(menu);
325
this._register(menu.onDidChange(e => {
326
const { primary, secondary } = this.getToolbarActions(menu);
327
if (!this.toolbar && (primary.length || secondary.length)) {
328
this.createToolbar();
329
}
330
this.toolbar.value!.setActions(primary, secondary);
331
}));
332
333
const { primary, secondary } = this.getToolbarActions(menu);
334
actions.push(...primary);
335
336
if (actions.length || secondary.length) {
337
this.createToolbar();
338
this.toolbar.value!.setActions(actions, secondary);
339
}
340
}
341
342
actionViewItemProvider(action: Action, options: IActionViewItemOptions) {
343
if (action.id === ToggleReactionsAction.ID) {
344
options = { label: false, icon: true };
345
} else {
346
options = { label: false, icon: true };
347
}
348
349
if (action.id === ReactionAction.ID) {
350
const item = new ReactionActionViewItem(action);
351
return item;
352
} else if (action instanceof MenuItemAction) {
353
return this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate });
354
} else if (action instanceof SubmenuItemAction) {
355
return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, options);
356
} else {
357
const item = new ActionViewItem({}, action, options);
358
return item;
359
}
360
}
361
362
async submitComment(): Promise<void> {
363
if (this._commentEditor && this._commentFormActions) {
364
await this._commentFormActions.triggerDefaultAction();
365
this.pendingEdit = undefined;
366
}
367
}
368
369
private createReactionPicker(reactionGroup: languages.CommentReaction[]): ToggleReactionsAction {
370
const toggleReactionAction = this._reactionActions.add(new ToggleReactionsAction(() => {
371
toggleReactionActionViewItem?.show();
372
}, nls.localize('commentToggleReaction', "Toggle Reaction")));
373
374
let reactionMenuActions: Action[] = [];
375
if (reactionGroup && reactionGroup.length) {
376
reactionMenuActions = reactionGroup.map((reaction) => {
377
return this._reactionActions.add(new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => {
378
try {
379
await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread, this.comment, reaction);
380
} catch (e) {
381
const error = e.message
382
? nls.localize('commentToggleReactionError', "Toggling the comment reaction failed: {0}.", e.message)
383
: nls.localize('commentToggleReactionDefaultError', "Toggling the comment reaction failed");
384
this.notificationService.error(error);
385
}
386
}));
387
});
388
}
389
390
toggleReactionAction.menuActions = reactionMenuActions;
391
392
const toggleReactionActionViewItem: DropdownMenuActionViewItem = this._reactionActions.add(new DropdownMenuActionViewItem(
393
toggleReactionAction,
394
(<ToggleReactionsAction>toggleReactionAction).menuActions,
395
this.contextMenuService,
396
{
397
actionViewItemProvider: (action, options) => {
398
if (action.id === ToggleReactionsAction.ID) {
399
return toggleReactionActionViewItem;
400
}
401
return this.actionViewItemProvider(action as Action, options);
402
},
403
classNames: 'toolbar-toggle-pickReactions',
404
anchorAlignmentProvider: () => AnchorAlignment.RIGHT
405
}
406
));
407
408
return toggleReactionAction;
409
}
410
411
private createReactionsContainer(commentDetailsContainer: HTMLElement): void {
412
this._reactionActionsContainer?.remove();
413
this._reactionsActionBar.clear();
414
this._reactionActions.clear();
415
416
const hasReactionHandler = this.commentService.hasReactionHandler(this.owner);
417
const reactions = this.comment.commentReactions?.filter(reaction => !!reaction.count) || [];
418
419
// Only create the container if there are reactions to show or if there's a reaction handler
420
if (reactions.length === 0 && !hasReactionHandler) {
421
return;
422
}
423
424
this._reactionActionsContainer = dom.append(commentDetailsContainer, dom.$('div.comment-reactions'));
425
this._reactionsActionBar.value = new ActionBar(this._reactionActionsContainer, {
426
actionViewItemProvider: (action, options) => {
427
if (action.id === ToggleReactionsAction.ID) {
428
return new DropdownMenuActionViewItem(
429
action,
430
(<ToggleReactionsAction>action).menuActions,
431
this.contextMenuService,
432
{
433
actionViewItemProvider: (action, options) => this.actionViewItemProvider(action as Action, options),
434
classNames: ['toolbar-toggle-pickReactions', ...ThemeIcon.asClassNameArray(Codicon.reactions)],
435
anchorAlignmentProvider: () => AnchorAlignment.RIGHT
436
}
437
);
438
}
439
return this.actionViewItemProvider(action as Action, options);
440
}
441
});
442
443
reactions.map(reaction => {
444
const action = this._reactionActions.add(new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && (reaction.canEdit || hasReactionHandler) ? 'active' : '', (reaction.canEdit || hasReactionHandler), async () => {
445
try {
446
await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread, this.comment, reaction);
447
} catch (e) {
448
let error: string;
449
450
if (reaction.hasReacted) {
451
error = e.message
452
? nls.localize('commentDeleteReactionError', "Deleting the comment reaction failed: {0}.", e.message)
453
: nls.localize('commentDeleteReactionDefaultError', "Deleting the comment reaction failed");
454
} else {
455
error = e.message
456
? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message)
457
: nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed");
458
}
459
this.notificationService.error(error);
460
}
461
}, reaction.reactors, reaction.iconPath, reaction.count));
462
463
this._reactionsActionBar.value?.push(action, { label: true, icon: true });
464
});
465
466
if (hasReactionHandler) {
467
const toggleReactionAction = this.createReactionPicker(this.comment.commentReactions || []);
468
this._reactionsActionBar.value?.push(toggleReactionAction, { label: false, icon: true });
469
}
470
}
471
472
get commentBodyValue(): string {
473
return (typeof this.comment.body === 'string') ? this.comment.body : this.comment.body.value;
474
}
475
476
private async createCommentEditor(editContainer: HTMLElement): Promise<void> {
477
this._editModeDisposables.clear();
478
const container = dom.append(editContainer, dom.$('.edit-textarea'));
479
this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, container, SimpleCommentEditor.getEditorOptions(this.configurationService), this._contextKeyService, this.parentThread);
480
this._editModeDisposables.add(this._commentEditor);
481
482
const resource = URI.from({
483
scheme: Schemas.commentsInput,
484
path: `/commentinput-${this.comment.uniqueIdInThread}-${Date.now()}.md`
485
});
486
const modelRef = await this.textModelService.createModelReference(resource);
487
this._commentEditorModel = modelRef;
488
this._editModeDisposables.add(this._commentEditorModel);
489
490
this._commentEditor.setModel(this._commentEditorModel.object.textEditorModel);
491
this._commentEditor.setValue(this.pendingEdit?.body ?? this.commentBodyValue);
492
if (this.pendingEdit) {
493
this._commentEditor.setPosition(this.pendingEdit.cursor);
494
} else {
495
const lastLine = this._commentEditorModel.object.textEditorModel.getLineCount();
496
const lastColumn = this._commentEditorModel.object.textEditorModel.getLineLength(lastLine) + 1;
497
this._commentEditor.setPosition(new Position(lastLine, lastColumn));
498
}
499
this.pendingEdit = undefined;
500
this._commentEditor.layout({ width: container.clientWidth - 14, height: this._editorHeight });
501
this._commentEditor.focus();
502
503
dom.scheduleAtNextAnimationFrame(dom.getWindow(editContainer), () => {
504
this._commentEditor!.layout({ width: container.clientWidth - 14, height: this._editorHeight });
505
this._commentEditor!.focus();
506
});
507
508
const commentThread = this.commentThread;
509
commentThread.input = {
510
uri: this._commentEditor.getModel()!.uri,
511
value: this.commentBodyValue
512
};
513
this.commentService.setActiveEditingCommentThread(commentThread);
514
this.commentService.setActiveCommentAndThread(this.owner, { thread: commentThread, comment: this.comment });
515
516
this._editModeDisposables.add(this._commentEditor.onDidFocusEditorWidget(() => {
517
commentThread.input = {
518
uri: this._commentEditor!.getModel()!.uri,
519
value: this.commentBodyValue
520
};
521
this.commentService.setActiveEditingCommentThread(commentThread);
522
this.commentService.setActiveCommentAndThread(this.owner, { thread: commentThread, comment: this.comment });
523
}));
524
525
this._editModeDisposables.add(this._commentEditor.onDidChangeModelContent(e => {
526
if (commentThread.input && this._commentEditor && this._commentEditor.getModel()!.uri === commentThread.input.uri) {
527
const newVal = this._commentEditor.getValue();
528
if (newVal !== commentThread.input.value) {
529
const input = commentThread.input;
530
input.value = newVal;
531
commentThread.input = input;
532
this.commentService.setActiveEditingCommentThread(commentThread);
533
this.commentService.setActiveCommentAndThread(this.owner, { thread: commentThread, comment: this.comment });
534
}
535
}
536
}));
537
538
this.calculateEditorHeight();
539
540
this._editModeDisposables.add((this._commentEditorModel.object.textEditorModel.onDidChangeContent(() => {
541
if (this._commentEditor && this.calculateEditorHeight()) {
542
this._commentEditor.layout({ height: this._editorHeight, width: this._commentEditor.getLayoutInfo().width });
543
this._commentEditor.render(true);
544
}
545
})));
546
547
}
548
549
private calculateEditorHeight(): boolean {
550
if (this._commentEditor) {
551
const newEditorHeight = calculateEditorHeight(this.parentEditor, this._commentEditor, this._editorHeight);
552
if (newEditorHeight !== this._editorHeight) {
553
this._editorHeight = newEditorHeight;
554
return true;
555
}
556
}
557
return false;
558
}
559
560
getPendingEdit(): languages.PendingComment | undefined {
561
const model = this._commentEditor?.getModel();
562
if (this._commentEditor && model && model.getValueLength() > 0) {
563
return { body: model.getValue(), cursor: this._commentEditor.getPosition()! };
564
}
565
return undefined;
566
}
567
568
private removeCommentEditor() {
569
this.isEditing = false;
570
if (this._editAction) {
571
this._editAction.enabled = true;
572
}
573
this._body.classList.remove('hidden');
574
this._editModeDisposables.clear();
575
this._commentEditor = null;
576
this._commentEditContainer!.remove();
577
}
578
579
layout(widthInPixel?: number) {
580
const editorWidth = widthInPixel !== undefined ? widthInPixel - 72 /* - margin and scrollbar*/ : (this._commentEditor?.getLayoutInfo().width ?? 0);
581
this._commentEditor?.layout({ width: editorWidth, height: this._editorHeight });
582
const scrollWidth = this._body.scrollWidth;
583
const width = dom.getContentWidth(this._body);
584
const scrollHeight = this._body.scrollHeight;
585
const height = dom.getContentHeight(this._body) + 4;
586
this._scrollableElement.setScrollDimensions({ width, scrollWidth, height, scrollHeight });
587
}
588
589
public async switchToEditMode() {
590
if (this.isEditing) {
591
return;
592
}
593
594
this.isEditing = true;
595
this._body.classList.add('hidden');
596
this._commentEditContainer = dom.append(this._commentDetailsContainer, dom.$('.edit-container'));
597
await this.createCommentEditor(this._commentEditContainer);
598
599
const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions'));
600
const otherActions = dom.append(formActions, dom.$('.other-actions'));
601
this.createCommentWidgetFormActions(otherActions);
602
const editorActions = dom.append(formActions, dom.$('.editor-actions'));
603
this.createCommentWidgetEditorActions(editorActions);
604
}
605
606
private readonly _editModeDisposables: DisposableStore = this._register(new DisposableStore());
607
private createCommentWidgetFormActions(container: HTMLElement) {
608
const menus = this.commentService.getCommentMenus(this.owner);
609
const menu = menus.getCommentActions(this.comment, this._contextKeyService);
610
611
this._editModeDisposables.add(menu);
612
this._editModeDisposables.add(menu.onDidChange(() => {
613
this._commentFormActions?.setActions(menu);
614
}));
615
616
this._commentFormActions = new CommentFormActions(this.keybindingService, this._contextKeyService, this.contextMenuService, container, (action: IAction): void => {
617
const text = this._commentEditor!.getValue();
618
619
action.run({
620
thread: this.commentThread,
621
commentUniqueId: this.comment.uniqueIdInThread,
622
text: text,
623
$mid: MarshalledId.CommentThreadNode
624
});
625
626
this.removeCommentEditor();
627
});
628
629
this._editModeDisposables.add(this._commentFormActions);
630
this._commentFormActions.setActions(menu);
631
}
632
633
private createCommentWidgetEditorActions(container: HTMLElement) {
634
const menus = this.commentService.getCommentMenus(this.owner);
635
const menu = menus.getCommentEditorActions(this._contextKeyService);
636
637
this._editModeDisposables.add(menu);
638
this._editModeDisposables.add(menu.onDidChange(() => {
639
this._commentEditorActions?.setActions(menu, true);
640
}));
641
642
this._commentEditorActions = new CommentFormActions(this.keybindingService, this._contextKeyService, this.contextMenuService, container, (action: IAction): void => {
643
const text = this._commentEditor!.getValue();
644
645
action.run({
646
thread: this.commentThread,
647
commentUniqueId: this.comment.uniqueIdInThread,
648
text: text,
649
$mid: MarshalledId.CommentThreadNode
650
});
651
652
this._commentEditor?.focus();
653
});
654
655
this._editModeDisposables.add(this._commentEditorActions);
656
this._commentEditorActions.setActions(menu, true);
657
}
658
659
setFocus(focused: boolean, visible: boolean = false) {
660
if (focused) {
661
this._domNode.focus();
662
this._actionsToolbarContainer.classList.add('tabfocused');
663
this._domNode.tabIndex = 0;
664
if (this.comment.mode === languages.CommentMode.Editing) {
665
this._commentEditor?.focus();
666
}
667
} else {
668
if (this._actionsToolbarContainer.classList.contains('tabfocused') && !this._actionsToolbarContainer.classList.contains('mouseover')) {
669
this._domNode.tabIndex = -1;
670
}
671
this._actionsToolbarContainer.classList.remove('tabfocused');
672
}
673
}
674
675
async update(newComment: languages.Comment) {
676
677
if (newComment.body !== this.comment.body) {
678
this.updateCommentBody(newComment.body);
679
}
680
681
if (this.comment.userIconPath && newComment.userIconPath && (URI.from(this.comment.userIconPath).toString() !== URI.from(newComment.userIconPath).toString())) {
682
this.updateCommentUserIcon(newComment.userIconPath);
683
}
684
685
const isChangingMode: boolean = newComment.mode !== undefined && newComment.mode !== this.comment.mode;
686
687
this.comment = newComment;
688
689
if (isChangingMode) {
690
if (newComment.mode === languages.CommentMode.Editing) {
691
await this.switchToEditMode();
692
} else {
693
this.removeCommentEditor();
694
}
695
}
696
697
if (newComment.label) {
698
this._isPendingLabel.innerText = newComment.label;
699
} else {
700
this._isPendingLabel.innerText = '';
701
}
702
703
// update comment reactions
704
this.createReactionsContainer(this._commentDetailsContainer);
705
706
if (this.comment.contextValue) {
707
this._commentContextValue.set(this.comment.contextValue);
708
} else {
709
this._commentContextValue.reset();
710
}
711
712
if (this.comment.timestamp) {
713
this.updateTimestamp(this.comment.timestamp);
714
}
715
}
716
717
private onContextMenu(e: MouseEvent) {
718
const event = new StandardMouseEvent(dom.getWindow(this._domNode), e);
719
this.contextMenuService.showContextMenu({
720
getAnchor: () => event,
721
menuId: MenuId.CommentThreadCommentContext,
722
menuActionOptions: { shouldForwardArgs: true },
723
contextKeyService: this._contextKeyService,
724
actionRunner: this._actionRunner,
725
getActionsContext: () => {
726
return this.commentNodeContext;
727
},
728
});
729
}
730
731
focus() {
732
this.domNode.focus();
733
if (!this._clearTimeout) {
734
this.domNode.classList.add('focus');
735
this._clearTimeout = setTimeout(() => {
736
this.domNode.classList.remove('focus');
737
}, 3000);
738
}
739
}
740
741
override dispose(): void {
742
super.dispose();
743
}
744
}
745
746
function fillInActions(groups: [string, Array<MenuItemAction | SubmenuItemAction>][], target: IAction[] | { primary: IAction[]; secondary: IAction[] }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void {
747
for (const tuple of groups) {
748
let [group, actions] = tuple;
749
if (useAlternativeActions) {
750
actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a);
751
}
752
753
if (isPrimaryGroup(group)) {
754
const to = Array.isArray(target) ? target : target.primary;
755
756
to.unshift(...actions);
757
} else {
758
const to = Array.isArray(target) ? target : target.secondary;
759
760
if (to.length > 0) {
761
to.push(new Separator());
762
}
763
764
to.push(...actions);
765
}
766
}
767
}
768
769