Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts
5241 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 dom from '../../../../base/browser/dom.js';
7
import * as nls from '../../../../nls.js';
8
import { renderMarkdown } from '../../../../base/browser/markdownRenderer.js';
9
import { IDisposable, DisposableStore } from '../../../../base/common/lifecycle.js';
10
import { IResourceLabel, ResourceLabels } from '../../../browser/labels.js';
11
import { CommentNode, ResourceWithCommentThreads } from '../common/commentModel.js';
12
import { ITreeContextMenuEvent, ITreeFilter, ITreeNode, TreeFilterResult, TreeVisibility } from '../../../../base/browser/ui/tree/tree.js';
13
import { IListVirtualDelegate, IListRenderer } from '../../../../base/browser/ui/list/list.js';
14
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
15
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
16
import { IListService, IWorkbenchAsyncDataTreeOptions, WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js';
17
import { IColorTheme, IThemeService } from '../../../../platform/theme/common/themeService.js';
18
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
19
import { TimestampWidget } from './timestamp.js';
20
import { Codicon } from '../../../../base/common/codicons.js';
21
import { ThemeIcon } from '../../../../base/common/themables.js';
22
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
23
import { commentViewThreadStateColorVar, getCommentThreadStateIconColor } from './commentColors.js';
24
import { CommentThreadApplicability, CommentThreadState, CommentState } from '../../../../editor/common/languages.js';
25
import { Color } from '../../../../base/common/color.js';
26
import { IMatch } from '../../../../base/common/filters.js';
27
import { FilterOptions } from './commentsFilterOptions.js';
28
import { basename } from '../../../../base/common/resources.js';
29
import { IStyleOverride } from '../../../../platform/theme/browser/defaultStyles.js';
30
import { IListStyles } from '../../../../base/browser/ui/list/listWidget.js';
31
import { ILocalizedString } from '../../../../platform/action/common/action.js';
32
import { CommentsModel } from './commentsModel.js';
33
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
34
import { ActionBar, IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js';
35
import { createActionViewItem, getContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
36
import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';
37
import { IAction } from '../../../../base/common/actions.js';
38
import { MarshalledId } from '../../../../base/common/marshallingIds.js';
39
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
40
import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
41
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
42
import { MarshalledCommentThread, MarshalledCommentThreadInternal } from '../../../common/comments.js';
43
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
44
45
export const COMMENTS_VIEW_ID = 'workbench.panel.comments';
46
export const COMMENTS_VIEW_STORAGE_ID = 'Comments';
47
export const COMMENTS_VIEW_TITLE: ILocalizedString = nls.localize2('comments.view.title', "Comments");
48
49
interface IResourceTemplateData {
50
resourceLabel: IResourceLabel;
51
separator: HTMLElement;
52
owner: HTMLElement;
53
}
54
55
interface ICommentThreadTemplateData {
56
threadMetadata: {
57
relevance: HTMLElement;
58
icon: HTMLElement;
59
userNames: HTMLSpanElement;
60
timestamp: TimestampWidget;
61
separator: HTMLElement;
62
commentPreview: HTMLSpanElement;
63
range: HTMLElement;
64
};
65
repliesMetadata: {
66
container: HTMLElement;
67
icon: HTMLElement;
68
count: HTMLSpanElement;
69
lastReplyDetail: HTMLSpanElement;
70
separator: HTMLElement;
71
timestamp: TimestampWidget;
72
};
73
actionBar: ActionBar;
74
disposables: IDisposable[];
75
}
76
77
class CommentsModelVirtualDelegate implements IListVirtualDelegate<ResourceWithCommentThreads | CommentNode> {
78
private static readonly RESOURCE_ID = 'resource-with-comments';
79
private static readonly COMMENT_ID = 'comment-node';
80
81
82
getHeight(element: any): number {
83
if ((element instanceof CommentNode) && element.hasReply()) {
84
return 44;
85
}
86
return 22;
87
}
88
89
public getTemplateId(element: any): string {
90
if (element instanceof ResourceWithCommentThreads) {
91
return CommentsModelVirtualDelegate.RESOURCE_ID;
92
}
93
if (element instanceof CommentNode) {
94
return CommentsModelVirtualDelegate.COMMENT_ID;
95
}
96
97
return '';
98
}
99
}
100
101
export class ResourceWithCommentsRenderer implements IListRenderer<ITreeNode<ResourceWithCommentThreads>, IResourceTemplateData> {
102
templateId: string = 'resource-with-comments';
103
104
constructor(
105
private labels: ResourceLabels
106
) {
107
}
108
109
renderTemplate(container: HTMLElement) {
110
const labelContainer = dom.append(container, dom.$('.resource-container'));
111
const resourceLabel = this.labels.create(labelContainer);
112
const separator = dom.append(labelContainer, dom.$('.separator'));
113
const owner = labelContainer.appendChild(dom.$('.owner'));
114
115
return { resourceLabel, owner, separator };
116
}
117
118
renderElement(node: ITreeNode<ResourceWithCommentThreads>, index: number, templateData: IResourceTemplateData): void {
119
templateData.resourceLabel.setFile(node.element.resource);
120
templateData.separator.innerText = '\u00b7';
121
122
if (node.element.ownerLabel) {
123
templateData.owner.innerText = node.element.ownerLabel;
124
templateData.separator.style.display = 'inline';
125
} else {
126
templateData.owner.innerText = '';
127
templateData.separator.style.display = 'none';
128
}
129
}
130
131
disposeTemplate(templateData: IResourceTemplateData): void {
132
templateData.resourceLabel.dispose();
133
}
134
}
135
136
export class CommentsMenus implements IDisposable {
137
private contextKeyService: IContextKeyService | undefined;
138
139
constructor(
140
@IMenuService private readonly menuService: IMenuService
141
) { }
142
143
getResourceActions(element: CommentNode): { actions: IAction[] } {
144
const actions = this.getActions(MenuId.CommentsViewThreadActions, element);
145
return { actions: actions.primary };
146
}
147
148
getResourceContextActions(element: CommentNode): IAction[] {
149
return this.getActions(MenuId.CommentsViewThreadActions, element).secondary;
150
}
151
152
public setContextKeyService(service: IContextKeyService) {
153
this.contextKeyService = service;
154
}
155
156
private getActions(menuId: MenuId, element: CommentNode): { primary: IAction[]; secondary: IAction[] } {
157
if (!this.contextKeyService) {
158
return { primary: [], secondary: [] };
159
}
160
161
const overlay: [string, any][] = [
162
['commentController', element.owner],
163
['resourceScheme', element.resource.scheme],
164
['commentThread', element.contextValue],
165
['canReply', element.thread.canReply]
166
];
167
const contextKeyService = this.contextKeyService.createOverlay(overlay);
168
169
const menu = this.menuService.getMenuActions(menuId, contextKeyService, { shouldForwardArgs: true });
170
return getContextMenuActions(menu, 'inline');
171
}
172
173
dispose() {
174
this.contextKeyService = undefined;
175
}
176
}
177
178
export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode>, ICommentThreadTemplateData> {
179
templateId: string = 'comment-node';
180
181
constructor(
182
private actionViewItemProvider: IActionViewItemProvider,
183
private menus: CommentsMenus,
184
@IConfigurationService private readonly configurationService: IConfigurationService,
185
@IHoverService private readonly hoverService: IHoverService,
186
@IThemeService private themeService: IThemeService
187
) { }
188
189
renderTemplate(container: HTMLElement) {
190
const threadContainer = dom.append(container, dom.$('.comment-thread-container'));
191
const metadataContainer = dom.append(threadContainer, dom.$('.comment-metadata-container'));
192
const metadata = dom.append(metadataContainer, dom.$('.comment-metadata'));
193
194
const icon = dom.append(metadata, dom.$('.icon'));
195
const userNames = dom.append(metadata, dom.$('.user'));
196
const timestamp = new TimestampWidget(this.configurationService, this.hoverService, dom.append(metadata, dom.$('.timestamp-container')));
197
const relevance = dom.append(metadata, dom.$('.relevance'));
198
const separator = dom.append(metadata, dom.$('.separator'));
199
const commentPreview = dom.append(metadata, dom.$('.text'));
200
const rangeContainer = dom.append(metadata, dom.$('.range'));
201
const range = dom.$('p');
202
rangeContainer.appendChild(range);
203
204
const threadMetadata = {
205
icon,
206
userNames,
207
timestamp,
208
relevance,
209
separator,
210
commentPreview,
211
range
212
};
213
threadMetadata.separator.innerText = '\u00b7';
214
215
const actionsContainer = dom.append(metadataContainer, dom.$('.actions'));
216
const actionBar = new ActionBar(actionsContainer, {
217
actionViewItemProvider: this.actionViewItemProvider
218
});
219
220
const snippetContainer = dom.append(threadContainer, dom.$('.comment-snippet-container'));
221
const repliesMetadata = {
222
container: snippetContainer,
223
icon: dom.append(snippetContainer, dom.$('.icon')),
224
count: dom.append(snippetContainer, dom.$('.count')),
225
lastReplyDetail: dom.append(snippetContainer, dom.$('.reply-detail')),
226
separator: dom.append(snippetContainer, dom.$('.separator')),
227
timestamp: new TimestampWidget(this.configurationService, this.hoverService, dom.append(snippetContainer, dom.$('.timestamp-container'))),
228
};
229
repliesMetadata.separator.innerText = '\u00b7';
230
repliesMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.indent));
231
232
const disposables = [threadMetadata.timestamp, repliesMetadata.timestamp];
233
return { threadMetadata, repliesMetadata, actionBar, disposables };
234
}
235
236
private getCountString(commentCount: number): string {
237
if (commentCount > 2) {
238
return nls.localize('commentsCountReplies', "{0} replies", commentCount - 1);
239
} else if (commentCount === 2) {
240
return nls.localize('commentsCountReply', "1 reply");
241
} else {
242
return nls.localize('commentCount', "1 comment");
243
}
244
}
245
246
private getRenderedComment(commentBody: IMarkdownString) {
247
const renderedComment = renderMarkdown(commentBody, {}, document.createElement('span'));
248
// eslint-disable-next-line no-restricted-syntax
249
const images = renderedComment.element.getElementsByTagName('img');
250
for (let i = 0; i < images.length; i++) {
251
const image = images[i];
252
const textDescription = dom.$('');
253
textDescription.textContent = image.alt ? nls.localize('imageWithLabel', "Image: {0}", image.alt) : nls.localize('image', "Image");
254
image.replaceWith(textDescription);
255
}
256
// eslint-disable-next-line no-restricted-syntax
257
const headings = [...renderedComment.element.getElementsByTagName('h1'), ...renderedComment.element.getElementsByTagName('h2'), ...renderedComment.element.getElementsByTagName('h3'), ...renderedComment.element.getElementsByTagName('h4'), ...renderedComment.element.getElementsByTagName('h5'), ...renderedComment.element.getElementsByTagName('h6')];
258
for (const heading of headings) {
259
const textNode = document.createTextNode(heading.textContent || '');
260
heading.replaceWith(textNode);
261
}
262
while ((renderedComment.element.children.length > 1) && (renderedComment.element.firstElementChild?.tagName === 'HR')) {
263
renderedComment.element.removeChild(renderedComment.element.firstElementChild);
264
}
265
return renderedComment;
266
}
267
268
private getIcon(threadState?: CommentThreadState, hasDraft?: boolean): ThemeIcon {
269
// Priority: draft > unresolved > resolved
270
if (hasDraft) {
271
return Codicon.commentDraft;
272
} else if (threadState === CommentThreadState.Unresolved) {
273
return Codicon.commentUnresolved;
274
} else {
275
return Codicon.comment;
276
}
277
}
278
279
renderElement(node: ITreeNode<CommentNode>, index: number, templateData: ICommentThreadTemplateData): void {
280
templateData.actionBar.clear();
281
282
const commentCount = node.element.replies.length + 1;
283
if (node.element.threadRelevance === CommentThreadApplicability.Outdated) {
284
templateData.threadMetadata.relevance.style.display = '';
285
templateData.threadMetadata.relevance.innerText = nls.localize('outdated', "Outdated");
286
templateData.threadMetadata.separator.style.display = 'none';
287
} else {
288
templateData.threadMetadata.relevance.innerText = '';
289
templateData.threadMetadata.relevance.style.display = 'none';
290
templateData.threadMetadata.separator.style.display = '';
291
}
292
293
templateData.threadMetadata.icon.classList.remove(...Array.from(templateData.threadMetadata.icon.classList.values())
294
.filter(value => value.startsWith('codicon')));
295
// Check if any comment in the thread has draft state
296
const hasDraft = node.element.thread.comments?.some(comment => comment.state === CommentState.Draft);
297
templateData.threadMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(this.getIcon(node.element.threadState, hasDraft)));
298
if (node.element.threadState !== undefined) {
299
const color = this.getCommentThreadWidgetStateColor(node.element.threadState, this.themeService.getColorTheme());
300
templateData.threadMetadata.icon.style.setProperty(commentViewThreadStateColorVar, `${color}`);
301
templateData.threadMetadata.icon.style.color = `var(${commentViewThreadStateColorVar})`;
302
}
303
templateData.threadMetadata.userNames.textContent = node.element.comment.userName;
304
templateData.threadMetadata.timestamp.setTimestamp(node.element.comment.timestamp ? new Date(node.element.comment.timestamp) : undefined);
305
const originalComment = node.element;
306
307
templateData.threadMetadata.commentPreview.innerText = '';
308
templateData.threadMetadata.commentPreview.style.height = '22px';
309
if (typeof originalComment.comment.body === 'string') {
310
templateData.threadMetadata.commentPreview.innerText = originalComment.comment.body;
311
} else {
312
const disposables = new DisposableStore();
313
templateData.disposables.push(disposables);
314
const renderedComment = this.getRenderedComment(originalComment.comment.body);
315
templateData.disposables.push(renderedComment);
316
for (let i = renderedComment.element.children.length - 1; i >= 1; i--) {
317
renderedComment.element.removeChild(renderedComment.element.children[i]);
318
}
319
templateData.threadMetadata.commentPreview.appendChild(renderedComment.element);
320
templateData.disposables.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), templateData.threadMetadata.commentPreview, renderedComment.element.textContent ?? ''));
321
}
322
323
if (node.element.range) {
324
if (node.element.range.startLineNumber === node.element.range.endLineNumber) {
325
templateData.threadMetadata.range.textContent = nls.localize('commentLine', "[Ln {0}]", node.element.range.startLineNumber);
326
} else {
327
templateData.threadMetadata.range.textContent = nls.localize('commentRange', "[Ln {0}-{1}]", node.element.range.startLineNumber, node.element.range.endLineNumber);
328
}
329
}
330
331
const menuActions = this.menus.getResourceActions(node.element);
332
templateData.actionBar.push(menuActions.actions, { icon: true, label: false });
333
templateData.actionBar.context = {
334
commentControlHandle: node.element.controllerHandle,
335
commentThreadHandle: node.element.threadHandle,
336
$mid: MarshalledId.CommentThread
337
} satisfies MarshalledCommentThread;
338
339
if (!node.element.hasReply()) {
340
templateData.repliesMetadata.container.style.display = 'none';
341
return;
342
}
343
344
templateData.repliesMetadata.container.style.display = '';
345
templateData.repliesMetadata.count.textContent = this.getCountString(commentCount);
346
const lastComment = node.element.replies[node.element.replies.length - 1].comment;
347
templateData.repliesMetadata.lastReplyDetail.textContent = nls.localize('lastReplyFrom', "Last reply from {0}", lastComment.userName);
348
templateData.repliesMetadata.timestamp.setTimestamp(lastComment.timestamp ? new Date(lastComment.timestamp) : undefined);
349
}
350
351
private getCommentThreadWidgetStateColor(state: CommentThreadState | undefined, theme: IColorTheme): Color | undefined {
352
return (state !== undefined) ? getCommentThreadStateIconColor(state, theme) : undefined;
353
}
354
355
disposeTemplate(templateData: ICommentThreadTemplateData): void {
356
templateData.disposables.forEach(disposeable => disposeable.dispose());
357
templateData.actionBar.dispose();
358
}
359
}
360
361
export interface ICommentsListOptions extends IWorkbenchAsyncDataTreeOptions<any, any> {
362
overrideStyles?: IStyleOverride<IListStyles>;
363
}
364
365
const enum FilterDataType {
366
Resource,
367
Comment
368
}
369
370
interface ResourceFilterData {
371
type: FilterDataType.Resource;
372
uriMatches: IMatch[];
373
}
374
375
interface CommentFilterData {
376
type: FilterDataType.Comment;
377
textMatches: IMatch[];
378
}
379
380
type FilterData = ResourceFilterData | CommentFilterData;
381
382
export class Filter implements ITreeFilter<ResourceWithCommentThreads | CommentNode, FilterData> {
383
384
constructor(public options: FilterOptions) { }
385
386
filter(element: ResourceWithCommentThreads | CommentNode, parentVisibility: TreeVisibility): TreeFilterResult<FilterData> {
387
if (this.options.filter === '' && this.options.showResolved && this.options.showUnresolved) {
388
return TreeVisibility.Visible;
389
}
390
391
if (element instanceof ResourceWithCommentThreads) {
392
return this.filterResourceMarkers(element);
393
} else {
394
return this.filterCommentNode(element, parentVisibility);
395
}
396
}
397
398
private filterResourceMarkers(resourceMarkers: ResourceWithCommentThreads): TreeFilterResult<FilterData> {
399
// Filter by text. Do not apply negated filters on resources instead use exclude patterns
400
if (this.options.textFilter.text && !this.options.textFilter.negate) {
401
const uriMatches = FilterOptions._filter(this.options.textFilter.text, basename(resourceMarkers.resource));
402
if (uriMatches) {
403
return { visibility: true, data: { type: FilterDataType.Resource, uriMatches: uriMatches || [] } };
404
}
405
}
406
407
return TreeVisibility.Recurse;
408
}
409
410
private filterCommentNode(comment: CommentNode, parentVisibility: TreeVisibility): TreeFilterResult<FilterData> {
411
const matchesResolvedState = (comment.threadState === undefined) || (this.options.showResolved && CommentThreadState.Resolved === comment.threadState) ||
412
(this.options.showUnresolved && CommentThreadState.Unresolved === comment.threadState);
413
414
if (!matchesResolvedState) {
415
return false;
416
}
417
418
if (!this.options.textFilter.text) {
419
return true;
420
}
421
422
const textMatches =
423
// Check body of comment for value
424
FilterOptions._messageFilter(this.options.textFilter.text, typeof comment.comment.body === 'string' ? comment.comment.body : comment.comment.body.value)
425
// Check first user for value
426
|| FilterOptions._messageFilter(this.options.textFilter.text, comment.comment.userName)
427
// Check all replies for value
428
|| (comment.replies.map(reply => {
429
// Check user for value
430
return FilterOptions._messageFilter(this.options.textFilter.text, reply.comment.userName)
431
// Check body of reply for value
432
|| FilterOptions._messageFilter(this.options.textFilter.text, typeof reply.comment.body === 'string' ? reply.comment.body : reply.comment.body.value);
433
}).filter(value => !!value) as IMatch[][]).flat();
434
435
// Matched and not negated
436
if (textMatches.length && !this.options.textFilter.negate) {
437
return { visibility: true, data: { type: FilterDataType.Comment, textMatches } };
438
}
439
440
// Matched and negated - exclude it only if parent visibility is not set
441
if (textMatches.length && this.options.textFilter.negate && parentVisibility === TreeVisibility.Recurse) {
442
return false;
443
}
444
445
// Not matched and negated - include it only if parent visibility is not set
446
if ((textMatches.length === 0) && this.options.textFilter.negate && parentVisibility === TreeVisibility.Recurse) {
447
return true;
448
}
449
450
return parentVisibility;
451
}
452
}
453
454
export class CommentsList extends WorkbenchObjectTree<CommentsModel | ResourceWithCommentThreads | CommentNode, any> {
455
private readonly menus: CommentsMenus;
456
457
constructor(
458
labels: ResourceLabels,
459
container: HTMLElement,
460
options: ICommentsListOptions,
461
@IContextKeyService contextKeyService: IContextKeyService,
462
@IListService listService: IListService,
463
@IInstantiationService instantiationService: IInstantiationService,
464
@IConfigurationService configurationService: IConfigurationService,
465
@IContextMenuService private readonly contextMenuService: IContextMenuService,
466
@IKeybindingService private readonly keybindingService: IKeybindingService
467
) {
468
const delegate = new CommentsModelVirtualDelegate();
469
const actionViewItemProvider = createActionViewItem.bind(undefined, instantiationService);
470
const menus = instantiationService.createInstance(CommentsMenus);
471
menus.setContextKeyService(contextKeyService);
472
const renderers = [
473
instantiationService.createInstance(ResourceWithCommentsRenderer, labels),
474
instantiationService.createInstance(CommentNodeRenderer, actionViewItemProvider, menus)
475
];
476
477
super(
478
'CommentsTree',
479
container,
480
delegate,
481
renderers,
482
{
483
accessibilityProvider: options.accessibilityProvider,
484
identityProvider: {
485
getId: (element: any) => {
486
if (element instanceof CommentsModel) {
487
return 'root';
488
}
489
if (element instanceof ResourceWithCommentThreads) {
490
return `${element.uniqueOwner}-${element.id}`;
491
}
492
if (element instanceof CommentNode) {
493
return `${element.uniqueOwner}-${element.resource.toString()}-${element.threadId}-${element.comment.uniqueIdInThread}` + (element.isRoot ? '-root' : '');
494
}
495
return '';
496
}
497
},
498
expandOnlyOnTwistieClick: true,
499
collapseByDefault: false,
500
overrideStyles: options.overrideStyles,
501
filter: options.filter,
502
sorter: options.sorter,
503
findWidgetEnabled: false,
504
multipleSelectionSupport: false,
505
},
506
instantiationService,
507
contextKeyService,
508
listService,
509
configurationService,
510
);
511
this.menus = menus;
512
this.disposables.add(this.onContextMenu(e => this.commentsOnContextMenu(e)));
513
}
514
515
private commentsOnContextMenu(treeEvent: ITreeContextMenuEvent<CommentsModel | ResourceWithCommentThreads | CommentNode | null>): void {
516
const node: CommentsModel | ResourceWithCommentThreads | CommentNode | null = treeEvent.element;
517
if (!(node instanceof CommentNode)) {
518
return;
519
}
520
const event: UIEvent = treeEvent.browserEvent;
521
522
event.preventDefault();
523
event.stopPropagation();
524
525
this.setFocus([node]);
526
const actions = this.menus.getResourceContextActions(node);
527
if (!actions.length) {
528
return;
529
}
530
this.contextMenuService.showContextMenu({
531
getAnchor: () => treeEvent.anchor,
532
getActions: () => actions,
533
getActionViewItem: (action) => {
534
const keybinding = this.keybindingService.lookupKeybinding(action.id);
535
if (keybinding) {
536
return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() });
537
}
538
return undefined;
539
},
540
onHide: (wasCancelled?: boolean) => {
541
if (wasCancelled) {
542
this.domFocus();
543
}
544
},
545
getActionsContext: (): MarshalledCommentThreadInternal => ({
546
commentControlHandle: node.controllerHandle,
547
commentThreadHandle: node.threadHandle,
548
$mid: MarshalledId.CommentThread,
549
thread: node.thread
550
})
551
});
552
}
553
554
filterComments(): void {
555
this.refilter();
556
}
557
558
getVisibleItemCount(): number {
559
let filtered = 0;
560
const root = this.getNode();
561
562
for (const resourceNode of root.children) {
563
for (const commentNode of resourceNode.children) {
564
if (commentNode.visible && resourceNode.visible) {
565
filtered++;
566
}
567
}
568
}
569
570
return filtered;
571
}
572
}
573
574