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