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