Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/comments/browser/commentsView.ts
5240 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 './media/panel.css';
7
import * as nls from '../../../../nls.js';
8
import * as dom from '../../../../base/browser/dom.js';
9
import { basename } from '../../../../base/common/resources.js';
10
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
11
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
12
import { CommentNode, ICommentThreadChangedEvent, ResourceWithCommentThreads } from '../common/commentModel.js';
13
import { ICommentService, IWorkspaceCommentThreadsEvent } from './commentService.js';
14
import { IEditorService } from '../../../services/editor/common/editorService.js';
15
import { ResourceLabels } from '../../../browser/labels.js';
16
import { CommentsList, COMMENTS_VIEW_TITLE, Filter } from './commentsTreeViewer.js';
17
import { IViewPaneOptions, FilterViewPane } from '../../../browser/parts/views/viewPane.js';
18
import { IViewDescriptorService } from '../../../common/views.js';
19
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
20
import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
21
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
22
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
23
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
24
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
25
import { CommentsViewFilterFocusContextKey, ICommentsView } from './comments.js';
26
import { CommentsFilters, CommentsFiltersChangeEvent, CommentsSortOrder } from './commentsViewActions.js';
27
import { Memento } from '../../../common/memento.js';
28
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
29
import { FilterOptions } from './commentsFilterOptions.js';
30
import { CommentThreadApplicability, CommentThreadState } from '../../../../editor/common/languages.js';
31
import { revealCommentThread } from './commentsController.js';
32
import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js';
33
import { CommentsModel, threadHasMeaningfulComments, type ICommentsModel } from './commentsModel.js';
34
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
35
import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';
36
import { AccessibleViewAction } from '../../accessibility/browser/accessibleViewActions.js';
37
import type { ITreeElement } from '../../../../base/browser/ui/tree/tree.js';
38
import { IPathService } from '../../../services/path/common/pathService.js';
39
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
40
import { URI } from '../../../../base/common/uri.js';
41
import { IRange } from '../../../../editor/common/core/range.js';
42
43
export const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey<boolean>('commentsView.hasComments', false);
44
export const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey<boolean>('commentsView.someCommentsExpanded', false);
45
export const CONTEXT_KEY_COMMENT_FOCUSED = new RawContextKey<boolean>('commentsView.commentFocused', false);
46
const VIEW_STORAGE_ID = 'commentsViewState';
47
48
interface CommentsViewState {
49
filter?: string;
50
filterHistory?: string[];
51
showResolved?: boolean;
52
showUnresolved?: boolean;
53
sortBy?: CommentsSortOrder;
54
}
55
56
type CommentsTreeNode = CommentsModel | ResourceWithCommentThreads | CommentNode;
57
58
function createResourceCommentsIterator(model: ICommentsModel): Iterable<ITreeElement<CommentsTreeNode>> {
59
const result: ITreeElement<CommentsTreeNode>[] = [];
60
61
for (const m of model.resourceCommentThreads) {
62
const children = [];
63
for (const r of m.commentThreads) {
64
if (threadHasMeaningfulComments(r.thread)) {
65
children.push({ element: r });
66
}
67
}
68
if (children.length > 0) {
69
result.push({ element: m, children });
70
}
71
}
72
return result;
73
}
74
75
export class CommentsPanel extends FilterViewPane implements ICommentsView {
76
private treeLabels!: ResourceLabels;
77
private tree: CommentsList | undefined;
78
private treeContainer!: HTMLElement;
79
private messageBoxContainer!: HTMLElement;
80
private totalComments: number = 0;
81
private readonly hasCommentsContextKey: IContextKey<boolean>;
82
private readonly someCommentsExpandedContextKey: IContextKey<boolean>;
83
private readonly commentsFocusedContextKey: IContextKey<boolean>;
84
private readonly filter: Filter;
85
readonly filters: CommentsFilters;
86
87
private currentHeight = 0;
88
private currentWidth = 0;
89
private readonly viewState: CommentsViewState;
90
private readonly stateMemento: Memento<CommentsViewState>;
91
private cachedFilterStats: { total: number; filtered: number } | undefined = undefined;
92
93
readonly onDidChangeVisibility = this.onDidChangeBodyVisibility;
94
95
get focusedCommentNode(): CommentNode | undefined {
96
const focused = this.tree?.getFocus();
97
if (focused?.length === 1 && focused[0] instanceof CommentNode) {
98
return focused[0];
99
}
100
return undefined;
101
}
102
103
get focusedCommentInfo(): string | undefined {
104
if (!this.focusedCommentNode) {
105
return;
106
}
107
return this.getScreenReaderInfoForNode(this.focusedCommentNode);
108
}
109
110
focusNextNode(): void {
111
if (!this.tree) {
112
return;
113
}
114
const focused = this.tree.getFocus()?.[0];
115
if (!focused) {
116
return;
117
}
118
let next = this.tree.navigate(focused).next();
119
while (next && !(next instanceof CommentNode)) {
120
next = this.tree.navigate(next).next();
121
}
122
if (!next) {
123
return;
124
}
125
this.tree.setFocus([next]);
126
}
127
128
focusPreviousNode(): void {
129
if (!this.tree) {
130
return;
131
}
132
const focused = this.tree.getFocus()?.[0];
133
if (!focused) {
134
return;
135
}
136
let previous = this.tree.navigate(focused).previous();
137
while (previous && !(previous instanceof CommentNode)) {
138
previous = this.tree.navigate(previous).previous();
139
}
140
if (!previous) {
141
return;
142
}
143
this.tree.setFocus([previous]);
144
}
145
146
constructor(
147
options: IViewPaneOptions,
148
@IInstantiationService instantiationService: IInstantiationService,
149
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
150
@IEditorService private readonly editorService: IEditorService,
151
@IConfigurationService configurationService: IConfigurationService,
152
@IContextKeyService contextKeyService: IContextKeyService,
153
@IContextMenuService contextMenuService: IContextMenuService,
154
@IKeybindingService keybindingService: IKeybindingService,
155
@IOpenerService openerService: IOpenerService,
156
@IThemeService themeService: IThemeService,
157
@ICommentService private readonly commentService: ICommentService,
158
@IHoverService hoverService: IHoverService,
159
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
160
@IStorageService storageService: IStorageService,
161
@IPathService private readonly pathService: IPathService,
162
) {
163
const stateMemento = new Memento<CommentsViewState>(VIEW_STORAGE_ID, storageService);
164
const viewState = stateMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE);
165
super({
166
...options,
167
filterOptions: {
168
placeholder: nls.localize('comments.filter.placeholder', "Filter (e.g. text, author)"),
169
ariaLabel: nls.localize('comments.filter.ariaLabel', "Filter comments"),
170
history: viewState.filterHistory || [],
171
text: viewState.filter || '',
172
focusContextKey: CommentsViewFilterFocusContextKey.key
173
}
174
}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
175
this.hasCommentsContextKey = CONTEXT_KEY_HAS_COMMENTS.bindTo(contextKeyService);
176
this.someCommentsExpandedContextKey = CONTEXT_KEY_SOME_COMMENTS_EXPANDED.bindTo(contextKeyService);
177
this.commentsFocusedContextKey = CONTEXT_KEY_COMMENT_FOCUSED.bindTo(contextKeyService);
178
this.stateMemento = stateMemento;
179
this.viewState = viewState;
180
181
this.filters = this._register(new CommentsFilters({
182
showResolved: this.viewState.showResolved !== false,
183
showUnresolved: this.viewState.showUnresolved !== false,
184
sortBy: this.viewState.sortBy ?? CommentsSortOrder.ResourceAscending,
185
}, this.contextKeyService));
186
this.filter = new Filter(new FilterOptions(this.filterWidget.getFilterText(), this.filters.showResolved, this.filters.showUnresolved));
187
188
this._register(this.filters.onDidChange((event: CommentsFiltersChangeEvent) => {
189
if (event.showResolved || event.showUnresolved) {
190
this.updateFilter();
191
}
192
if (event.sortBy) {
193
this.refresh();
194
}
195
}));
196
this._register(this.filterWidget.onDidChangeFilterText(() => this.updateFilter()));
197
}
198
199
override saveState(): void {
200
this.viewState.filter = this.filterWidget.getFilterText();
201
this.viewState.filterHistory = this.filterWidget.getHistory();
202
this.viewState.showResolved = this.filters.showResolved;
203
this.viewState.showUnresolved = this.filters.showUnresolved;
204
this.viewState.sortBy = this.filters.sortBy;
205
this.stateMemento.saveMemento();
206
super.saveState();
207
}
208
209
override render(): void {
210
super.render();
211
this._register(registerNavigableContainer({
212
name: 'commentsView',
213
focusNotifiers: [this, this.filterWidget],
214
focusNextWidget: () => {
215
if (this.filterWidget.hasFocus()) {
216
this.focus();
217
}
218
},
219
focusPreviousWidget: () => {
220
if (!this.filterWidget.hasFocus()) {
221
this.focusFilter();
222
}
223
}
224
}));
225
}
226
227
public focusFilter(): void {
228
this.filterWidget.focus();
229
}
230
231
public clearFilterText(): void {
232
this.filterWidget.setFilterText('');
233
}
234
235
public getFilterStats(): { total: number; filtered: number } {
236
if (!this.cachedFilterStats) {
237
this.cachedFilterStats = {
238
total: this.totalComments,
239
filtered: this.tree?.getVisibleItemCount() ?? 0
240
};
241
}
242
243
return this.cachedFilterStats;
244
}
245
246
private updateFilter() {
247
this.filter.options = new FilterOptions(this.filterWidget.getFilterText(), this.filters.showResolved, this.filters.showUnresolved);
248
this.tree?.filterComments();
249
250
this.cachedFilterStats = undefined;
251
const { total, filtered } = this.getFilterStats();
252
this.filterWidget.updateBadge(total === filtered || total === 0 ? undefined : nls.localize('showing filtered results', "Showing {0} of {1}", filtered, total));
253
this.filterWidget.checkMoreFilters(!this.filters.showResolved || !this.filters.showUnresolved);
254
}
255
256
protected override renderBody(container: HTMLElement): void {
257
super.renderBody(container);
258
259
container.classList.add('comments-panel');
260
261
const domContainer = dom.append(container, dom.$('.comments-panel-container'));
262
263
this.treeContainer = dom.append(domContainer, dom.$('.tree-container'));
264
this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons');
265
266
this.cachedFilterStats = undefined;
267
this.createTree();
268
this.createMessageBox(domContainer);
269
270
this._register(this.commentService.onDidSetAllCommentThreads(this.onAllCommentsChanged, this));
271
this._register(this.commentService.onDidUpdateCommentThreads(this.onCommentsUpdated, this));
272
this._register(this.commentService.onDidDeleteDataProvider(this.onDataProviderDeleted, this));
273
274
this._register(this.onDidChangeBodyVisibility(visible => {
275
if (visible) {
276
this.refresh();
277
}
278
}));
279
280
this.renderComments();
281
}
282
283
public override focus(): void {
284
super.focus();
285
286
const element = this.tree?.getHTMLElement();
287
if (element && dom.isActiveElement(element)) {
288
return;
289
}
290
291
if (!this.commentService.commentsModel.hasCommentThreads() && this.messageBoxContainer) {
292
this.messageBoxContainer.focus();
293
} else if (this.tree) {
294
this.tree.domFocus();
295
}
296
}
297
298
private renderComments(): void {
299
this.treeContainer.classList.toggle('hidden', !this.commentService.commentsModel.hasCommentThreads());
300
this.renderMessage();
301
this.tree?.setChildren(null, createResourceCommentsIterator(this.commentService.commentsModel));
302
}
303
304
public collapseAll() {
305
if (this.tree) {
306
this.tree.collapseAll();
307
this.tree.setSelection([]);
308
this.tree.setFocus([]);
309
this.tree.domFocus();
310
this.tree.focusFirst();
311
}
312
}
313
314
public expandAll() {
315
if (this.tree) {
316
this.tree.expandAll();
317
this.tree.setSelection([]);
318
this.tree.setFocus([]);
319
this.tree.domFocus();
320
this.tree.focusFirst();
321
}
322
}
323
324
public get hasRendered(): boolean {
325
return !!this.tree;
326
}
327
328
protected layoutBodyContent(height: number = this.currentHeight, width: number = this.currentWidth): void {
329
if (this.messageBoxContainer) {
330
this.messageBoxContainer.style.height = `${height}px`;
331
}
332
this.tree?.layout(height, width);
333
this.currentHeight = height;
334
this.currentWidth = width;
335
}
336
337
private createMessageBox(parent: HTMLElement): void {
338
this.messageBoxContainer = dom.append(parent, dom.$('.message-box-container'));
339
this.messageBoxContainer.setAttribute('tabIndex', '0');
340
}
341
342
private renderMessage(): void {
343
this.messageBoxContainer.textContent = this.commentService.commentsModel.getMessage();
344
this.messageBoxContainer.classList.toggle('hidden', this.commentService.commentsModel.hasCommentThreads());
345
}
346
347
private makeCommentLocationLabel(file: URI, range?: IRange) {
348
const fileLabel = basename(file);
349
if (!range) {
350
return nls.localize('fileCommentLabel', "in {0}", fileLabel);
351
}
352
if (range.startLineNumber === range.endLineNumber) {
353
return nls.localize('oneLineCommentLabel', "at line {0} column {1} in {2}", range.startLineNumber, range.startColumn, fileLabel);
354
} else {
355
return nls.localize('multiLineCommentLabel', "from line {0} to line {1} in {2}", range.startLineNumber, range.endLineNumber, fileLabel);
356
}
357
}
358
359
private makeScreenReaderLabelInfo(element: CommentNode, forAriaLabel?: boolean) {
360
const userName = element.comment.userName;
361
const locationLabel = this.makeCommentLocationLabel(element.resource, element.range);
362
const replyCountLabel = this.getReplyCountAsString(element, forAriaLabel);
363
const bodyLabel = (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value;
364
365
return { userName, locationLabel, replyCountLabel, bodyLabel };
366
}
367
368
private getScreenReaderInfoForNode(element: CommentNode, forAriaLabel?: boolean): string {
369
let accessibleViewHint = '';
370
if (forAriaLabel && this.configurationService.getValue(AccessibilityVerbositySettingId.Comments)) {
371
const kbLabel = this.keybindingService.lookupKeybinding(AccessibleViewAction.id)?.getAriaLabel();
372
accessibleViewHint = kbLabel ? nls.localize('accessibleViewHint', "\nInspect this in the accessible view ({0}).", kbLabel) : nls.localize('acessibleViewHintNoKbOpen', "\nInspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding.");
373
}
374
const replies = this.getRepliesAsString(element, forAriaLabel);
375
const editor = this.editorService.findEditors(element.resource);
376
const codeEditor = this.editorService.activeEditorPane?.getControl();
377
let relevantLines;
378
if (element.range && editor?.length && isCodeEditor(codeEditor)) {
379
relevantLines = codeEditor.getModel()?.getValueInRange(element.range);
380
if (relevantLines) {
381
relevantLines = '\nCorresponding code: \n' + relevantLines;
382
}
383
}
384
if (!relevantLines) {
385
relevantLines = '';
386
}
387
388
const labelInfo = this.makeScreenReaderLabelInfo(element, forAriaLabel);
389
390
if (element.threadRelevance === CommentThreadApplicability.Outdated) {
391
return nls.localize('resourceWithCommentLabelOutdated',
392
"Outdated from {0}: {1}\n{2}\n{3}\n{4}",
393
labelInfo.userName,
394
labelInfo.bodyLabel,
395
labelInfo.locationLabel,
396
labelInfo.replyCountLabel,
397
relevantLines
398
) + replies + accessibleViewHint;
399
} else {
400
return nls.localize('resourceWithCommentLabel',
401
"{0}: {1}\n{2}\n{3}\n{4}",
402
labelInfo.userName,
403
labelInfo.bodyLabel,
404
labelInfo.locationLabel,
405
labelInfo.replyCountLabel,
406
relevantLines
407
) + replies + accessibleViewHint;
408
}
409
}
410
411
private getRepliesAsString(node: CommentNode, forAriaLabel?: boolean): string {
412
if (!node.replies.length || forAriaLabel) {
413
return '';
414
}
415
return '\n' + node.replies.map(reply => nls.localize('resourceWithRepliesLabel',
416
"{0} {1}",
417
reply.comment.userName,
418
(typeof reply.comment.body === 'string') ? reply.comment.body : reply.comment.body.value)
419
).join('\n');
420
}
421
422
private getReplyCountAsString(node: CommentNode, forAriaLabel?: boolean): string {
423
return node.replies.length && !forAriaLabel ? nls.localize('replyCount', " {0} replies,", node.replies.length) : '';
424
}
425
426
private createTree(): void {
427
this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this));
428
this.tree = this._register(this.instantiationService.createInstance(CommentsList, this.treeLabels, this.treeContainer, {
429
overrideStyles: this.getLocationBasedColors().listOverrideStyles,
430
selectionNavigation: true,
431
filter: this.filter,
432
sorter: {
433
compare: (a: CommentsTreeNode, b: CommentsTreeNode) => {
434
if (a instanceof CommentsModel || b instanceof CommentsModel) {
435
return 0;
436
}
437
if (this.filters.sortBy === CommentsSortOrder.UpdatedAtDescending) {
438
return a.lastUpdatedAt > b.lastUpdatedAt ? -1 : 1;
439
} else if (this.filters.sortBy === CommentsSortOrder.ResourceAscending) {
440
if (a instanceof ResourceWithCommentThreads && b instanceof ResourceWithCommentThreads) {
441
const workspaceScheme = this.pathService.defaultUriScheme;
442
if ((a.resource.scheme !== b.resource.scheme) && (a.resource.scheme === workspaceScheme || b.resource.scheme === workspaceScheme)) {
443
// Workspace scheme should always come first
444
return b.resource.scheme === workspaceScheme ? 1 : -1;
445
}
446
return a.resource.toString() > b.resource.toString() ? 1 : -1;
447
} else if (a instanceof CommentNode && b instanceof CommentNode && a.thread.range && b.thread.range) {
448
return a.thread.range?.startLineNumber > b.thread.range?.startLineNumber ? 1 : -1;
449
}
450
}
451
return 0;
452
},
453
},
454
keyboardNavigationLabelProvider: {
455
getKeyboardNavigationLabel: (item: CommentsTreeNode) => {
456
return undefined;
457
}
458
},
459
accessibilityProvider: {
460
getAriaLabel: (element: any): string => {
461
if (element instanceof CommentsModel) {
462
return nls.localize('rootCommentsLabel', "Comments for current workspace");
463
}
464
if (element instanceof ResourceWithCommentThreads) {
465
return nls.localize('resourceWithCommentThreadsLabel', "Comments in {0}, full path {1}", basename(element.resource), element.resource.fsPath);
466
}
467
if (element instanceof CommentNode) {
468
return this.getScreenReaderInfoForNode(element, true);
469
}
470
return '';
471
},
472
getWidgetAriaLabel(): string {
473
return COMMENTS_VIEW_TITLE.value;
474
}
475
}
476
}));
477
478
this._register(this.tree.onDidOpen(e => {
479
this.openFile(e.element, e.editorOptions.pinned, e.editorOptions.preserveFocus, e.sideBySide);
480
}));
481
482
483
this._register(this.tree.onDidChangeModel(() => {
484
this.updateSomeCommentsExpanded();
485
}));
486
this._register(this.tree.onDidChangeCollapseState(() => {
487
this.updateSomeCommentsExpanded();
488
}));
489
this._register(this.tree.onDidFocus(() => this.commentsFocusedContextKey.set(true)));
490
this._register(this.tree.onDidBlur(() => this.commentsFocusedContextKey.set(false)));
491
}
492
493
private openFile(element: any, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): void {
494
if (!element) {
495
return;
496
}
497
498
if (!(element instanceof ResourceWithCommentThreads || element instanceof CommentNode)) {
499
return;
500
}
501
const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].thread : element.thread;
502
const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment : undefined;
503
return revealCommentThread(this.commentService, this.editorService, this.uriIdentityService, threadToReveal, commentToReveal, false, pinned, preserveFocus, sideBySide);
504
}
505
506
private async refresh(): Promise<void> {
507
if (!this.tree) {
508
return;
509
}
510
if (this.isVisible()) {
511
this.hasCommentsContextKey.set(this.commentService.commentsModel.hasCommentThreads());
512
this.cachedFilterStats = undefined;
513
this.renderComments();
514
515
if (this.tree.getSelection().length === 0 && this.commentService.commentsModel.hasCommentThreads()) {
516
const firstComment = this.commentService.commentsModel.resourceCommentThreads[0].commentThreads[0];
517
if (firstComment) {
518
this.tree.setFocus([firstComment]);
519
this.tree.setSelection([firstComment]);
520
}
521
}
522
}
523
}
524
525
private onAllCommentsChanged(e: IWorkspaceCommentThreadsEvent): void {
526
this.cachedFilterStats = undefined;
527
this.totalComments += e.commentThreads.length;
528
529
let unresolved = 0;
530
for (const thread of e.commentThreads) {
531
if (thread.state === CommentThreadState.Unresolved) {
532
unresolved++;
533
}
534
}
535
this.refresh();
536
}
537
538
private onCommentsUpdated(e: ICommentThreadChangedEvent): void {
539
this.cachedFilterStats = undefined;
540
541
this.totalComments += e.added.length;
542
this.totalComments -= e.removed.length;
543
544
let unresolved = 0;
545
for (const resource of this.commentService.commentsModel.resourceCommentThreads) {
546
for (const thread of resource.commentThreads) {
547
if (thread.threadState === CommentThreadState.Unresolved) {
548
unresolved++;
549
}
550
}
551
}
552
this.refresh();
553
}
554
555
private onDataProviderDeleted(owner: string | undefined): void {
556
this.cachedFilterStats = undefined;
557
this.totalComments = 0;
558
this.refresh();
559
}
560
561
private updateSomeCommentsExpanded() {
562
this.someCommentsExpandedContextKey.set(this.isSomeCommentsExpanded());
563
}
564
565
public areAllCommentsExpanded(): boolean {
566
if (!this.tree) {
567
return false;
568
}
569
const navigator = this.tree.navigate();
570
while (navigator.next()) {
571
if (this.tree.isCollapsed(navigator.current())) {
572
return false;
573
}
574
}
575
return true;
576
}
577
578
public isSomeCommentsExpanded(): boolean {
579
if (!this.tree) {
580
return false;
581
}
582
const navigator = this.tree.navigate();
583
while (navigator.next()) {
584
if (!this.tree.isCollapsed(navigator.current())) {
585
return true;
586
}
587
}
588
return false;
589
}
590
}
591
592