Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.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 { CancellationToken } from '../../../../../base/common/cancellation.js';
7
import { IDiffResult } from '../../../../../base/common/diff/diff.js';
8
import { Emitter, type IValueWithChangeEvent } from '../../../../../base/common/event.js';
9
import { Disposable, DisposableStore, dispose } from '../../../../../base/common/lifecycle.js';
10
import { Schemas } from '../../../../../base/common/network.js';
11
import type { URI } from '../../../../../base/common/uri.js';
12
import { FontInfo } from '../../../../../editor/common/config/fontInfo.js';
13
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
14
import type { ContextKeyValue } from '../../../../../platform/contextkey/common/contextkey.js';
15
import { MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js';
16
import { DiffElementCellViewModelBase, DiffElementPlaceholderViewModel, IDiffElementViewModelBase, NotebookDocumentMetadataViewModel, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from './diffElementViewModel.js';
17
import { NotebookDiffEditorEventDispatcher } from './eventDispatcher.js';
18
import { INotebookDiffViewModel, INotebookDiffViewModelUpdateEvent, NOTEBOOK_DIFF_ITEM_DIFF_STATE, NOTEBOOK_DIFF_ITEM_KIND } from './notebookDiffEditorBrowser.js';
19
import { NotebookTextModel } from '../../common/model/notebookTextModel.js';
20
import { CellUri, INotebookDiffEditorModel } from '../../common/notebookCommon.js';
21
import { INotebookService } from '../../common/notebookService.js';
22
import { INotebookEditorWorkerService } from '../../common/services/notebookWorkerService.js';
23
import { IDiffEditorHeightCalculatorService } from './editorHeightCalculator.js';
24
import { raceCancellation } from '../../../../../base/common/async.js';
25
import { computeDiff } from '../../common/notebookDiff.js';
26
27
export class NotebookDiffViewModel extends Disposable implements INotebookDiffViewModel, IValueWithChangeEvent<readonly MultiDiffEditorItem[]> {
28
private readonly placeholderAndRelatedCells = new Map<DiffElementPlaceholderViewModel, DiffElementCellViewModelBase[]>();
29
private readonly _items: IDiffElementViewModelBase[] = [];
30
get items(): readonly IDiffElementViewModelBase[] {
31
return this._items;
32
}
33
private readonly _onDidChangeItems = this._register(new Emitter<INotebookDiffViewModelUpdateEvent>());
34
public readonly onDidChangeItems = this._onDidChangeItems.event;
35
private readonly disposables = this._register(new DisposableStore());
36
private _onDidChange = this._register(new Emitter<void>());
37
private diffEditorItems: NotebookMultiDiffEditorItem[] = [];
38
public onDidChange = this._onDidChange.event;
39
private notebookMetadataViewModel?: NotebookDocumentMetadataViewModel;
40
get value(): readonly NotebookMultiDiffEditorItem[] {
41
return this.diffEditorItems
42
.filter(item => item.type !== 'placeholder')
43
.filter(item => {
44
if (this._includeUnchanged) {
45
return true;
46
}
47
if (item instanceof NotebookMultiDiffEditorCellItem) {
48
return item.type === 'unchanged' && item.containerType === 'unchanged' ? false : true;
49
}
50
if (item instanceof NotebookMultiDiffEditorMetadataItem) {
51
return item.type === 'unchanged' && item.containerType === 'unchanged' ? false : true;
52
}
53
if (item instanceof NotebookMultiDiffEditorOutputItem) {
54
return item.type === 'unchanged' && item.containerType === 'unchanged' ? false : true;
55
}
56
return true;
57
})
58
.filter(item => item instanceof NotebookMultiDiffEditorOutputItem ? !this.hideOutput : true)
59
.filter(item => item instanceof NotebookMultiDiffEditorMetadataItem ? !this.ignoreMetadata : true);
60
}
61
62
private _hasUnchangedCells?: boolean;
63
public get hasUnchangedCells() {
64
return this._hasUnchangedCells === true;
65
}
66
private _includeUnchanged?: boolean;
67
public get includeUnchanged() {
68
return this._includeUnchanged === true;
69
}
70
public set includeUnchanged(value) {
71
this._includeUnchanged = value;
72
this._onDidChange.fire();
73
}
74
private hideOutput?: boolean;
75
private ignoreMetadata?: boolean;
76
77
private originalCellViewModels: IDiffElementViewModelBase[] = [];
78
constructor(private readonly model: INotebookDiffEditorModel,
79
private readonly notebookEditorWorkerService: INotebookEditorWorkerService,
80
private readonly configurationService: IConfigurationService,
81
private readonly eventDispatcher: NotebookDiffEditorEventDispatcher,
82
private readonly notebookService: INotebookService,
83
private readonly diffEditorHeightCalculator: IDiffEditorHeightCalculatorService,
84
private readonly fontInfo?: FontInfo,
85
private readonly excludeUnchangedPlaceholder?: boolean,
86
) {
87
super();
88
this.hideOutput = this.model.modified.notebook.transientOptions.transientOutputs || this.configurationService.getValue<boolean>('notebook.diff.ignoreOutputs');
89
this.ignoreMetadata = this.configurationService.getValue('notebook.diff.ignoreMetadata');
90
91
this._register(this.configurationService.onDidChangeConfiguration(e => {
92
let triggerChange = false;
93
let metadataChanged = false;
94
if (e.affectsConfiguration('notebook.diff.ignoreMetadata')) {
95
const newValue = this.configurationService.getValue<boolean>('notebook.diff.ignoreMetadata');
96
97
if (newValue !== undefined && this.ignoreMetadata !== newValue) {
98
this.ignoreMetadata = newValue;
99
triggerChange = true;
100
metadataChanged = true;
101
}
102
}
103
104
if (e.affectsConfiguration('notebook.diff.ignoreOutputs')) {
105
const newValue = this.configurationService.getValue<boolean>('notebook.diff.ignoreOutputs');
106
107
if (newValue !== undefined && this.hideOutput !== (newValue || this.model.modified.notebook.transientOptions.transientOutputs)) {
108
this.hideOutput = newValue || !!(this.model.modified.notebook.transientOptions.transientOutputs);
109
triggerChange = true;
110
}
111
}
112
113
if (metadataChanged) {
114
this.toggleNotebookMetadata();
115
}
116
if (triggerChange) {
117
this._onDidChange.fire();
118
}
119
}));
120
}
121
override dispose() {
122
this.clear();
123
super.dispose();
124
}
125
private clear() {
126
this.disposables.clear();
127
dispose(Array.from(this.placeholderAndRelatedCells.keys()));
128
this.placeholderAndRelatedCells.clear();
129
dispose(this.originalCellViewModels);
130
this.originalCellViewModels = [];
131
dispose(this._items);
132
this._items.splice(0, this._items.length);
133
}
134
135
async computeDiff(token: CancellationToken): Promise<void> {
136
const diffResult = await raceCancellation(this.notebookEditorWorkerService.computeDiff(this.model.original.resource, this.model.modified.resource), token);
137
if (!diffResult || token.isCancellationRequested) {
138
// after await the editor might be disposed.
139
return;
140
}
141
142
prettyChanges(this.model.original.notebook, this.model.modified.notebook, diffResult.cellsDiff);
143
144
const { cellDiffInfo, firstChangeIndex } = computeDiff(this.model.original.notebook, this.model.modified.notebook, diffResult);
145
if (isEqual(cellDiffInfo, this.originalCellViewModels, this.model)) {
146
return;
147
} else {
148
await raceCancellation(this.updateViewModels(cellDiffInfo, diffResult.metadataChanged, firstChangeIndex), token);
149
if (token.isCancellationRequested) {
150
return;
151
}
152
this.updateDiffEditorItems();
153
}
154
}
155
156
private toggleNotebookMetadata() {
157
if (!this.notebookMetadataViewModel) {
158
return;
159
}
160
161
if (this.ignoreMetadata) {
162
if (this._items.length && this._items[0] === this.notebookMetadataViewModel) {
163
this._items.splice(0, 1);
164
this._onDidChangeItems.fire({ start: 0, deleteCount: 1, elements: [] });
165
}
166
} else {
167
if (!this._items.length || this._items[0] !== this.notebookMetadataViewModel) {
168
this._items.splice(0, 0, this.notebookMetadataViewModel);
169
this._onDidChangeItems.fire({ start: 0, deleteCount: 0, elements: [this.notebookMetadataViewModel] });
170
}
171
}
172
}
173
private updateDiffEditorItems() {
174
this.diffEditorItems = [];
175
const originalSourceUri = this.model.original.resource!;
176
const modifiedSourceUri = this.model.modified.resource!;
177
this._hasUnchangedCells = false;
178
this.items.forEach(item => {
179
switch (item.type) {
180
case 'delete': {
181
this.diffEditorItems.push(new NotebookMultiDiffEditorCellItem(item.original!.uri, undefined, item.type, item.type));
182
const originalMetadata = CellUri.generateCellPropertyUri(originalSourceUri, item.original!.handle, Schemas.vscodeNotebookCellMetadata);
183
this.diffEditorItems.push(new NotebookMultiDiffEditorMetadataItem(originalMetadata, undefined, item.type, item.type));
184
const originalOutput = CellUri.generateCellPropertyUri(originalSourceUri, item.original!.handle, Schemas.vscodeNotebookCellOutput);
185
this.diffEditorItems.push(new NotebookMultiDiffEditorOutputItem(originalOutput, undefined, item.type, item.type));
186
break;
187
}
188
case 'insert': {
189
this.diffEditorItems.push(new NotebookMultiDiffEditorCellItem(undefined, item.modified!.uri, item.type, item.type));
190
const modifiedMetadata = CellUri.generateCellPropertyUri(modifiedSourceUri, item.modified!.handle, Schemas.vscodeNotebookCellMetadata);
191
this.diffEditorItems.push(new NotebookMultiDiffEditorMetadataItem(undefined, modifiedMetadata, item.type, item.type));
192
const modifiedOutput = CellUri.generateCellPropertyUri(modifiedSourceUri, item.modified!.handle, Schemas.vscodeNotebookCellOutput);
193
this.diffEditorItems.push(new NotebookMultiDiffEditorOutputItem(undefined, modifiedOutput, item.type, item.type));
194
break;
195
}
196
case 'modified': {
197
const cellType = item.checkIfInputModified() ? item.type : 'unchanged';
198
const containerChanged = (item.checkIfInputModified() || item.checkMetadataIfModified() || item.checkIfOutputsModified()) ? item.type : 'unchanged';
199
this.diffEditorItems.push(new NotebookMultiDiffEditorCellItem(item.original!.uri, item.modified!.uri, cellType, containerChanged));
200
const originalMetadata = CellUri.generateCellPropertyUri(originalSourceUri, item.original!.handle, Schemas.vscodeNotebookCellMetadata);
201
const modifiedMetadata = CellUri.generateCellPropertyUri(modifiedSourceUri, item.modified!.handle, Schemas.vscodeNotebookCellMetadata);
202
this.diffEditorItems.push(new NotebookMultiDiffEditorMetadataItem(originalMetadata, modifiedMetadata, item.checkMetadataIfModified() ? item.type : 'unchanged', containerChanged));
203
const originalOutput = CellUri.generateCellPropertyUri(originalSourceUri, item.original!.handle, Schemas.vscodeNotebookCellOutput);
204
const modifiedOutput = CellUri.generateCellPropertyUri(modifiedSourceUri, item.modified!.handle, Schemas.vscodeNotebookCellOutput);
205
this.diffEditorItems.push(new NotebookMultiDiffEditorOutputItem(originalOutput, modifiedOutput, item.checkIfOutputsModified() ? item.type : 'unchanged', containerChanged));
206
break;
207
}
208
case 'unchanged': {
209
this._hasUnchangedCells = true;
210
this.diffEditorItems.push(new NotebookMultiDiffEditorCellItem(item.original!.uri, item.modified!.uri, item.type, item.type));
211
const originalMetadata = CellUri.generateCellPropertyUri(originalSourceUri, item.original!.handle, Schemas.vscodeNotebookCellMetadata);
212
const modifiedMetadata = CellUri.generateCellPropertyUri(modifiedSourceUri, item.modified!.handle, Schemas.vscodeNotebookCellMetadata);
213
this.diffEditorItems.push(new NotebookMultiDiffEditorMetadataItem(originalMetadata, modifiedMetadata, item.type, item.type));
214
const originalOutput = CellUri.generateCellPropertyUri(originalSourceUri, item.original!.handle, Schemas.vscodeNotebookCellOutput);
215
const modifiedOutput = CellUri.generateCellPropertyUri(modifiedSourceUri, item.modified!.handle, Schemas.vscodeNotebookCellOutput);
216
this.diffEditorItems.push(new NotebookMultiDiffEditorOutputItem(originalOutput, modifiedOutput, item.type, item.type));
217
break;
218
}
219
}
220
});
221
222
this._onDidChange.fire();
223
}
224
225
private async updateViewModels(cellDiffInfo: CellDiffInfo[], metadataChanged: boolean, firstChangeIndex: number) {
226
const cellViewModels = await this.createDiffViewModels(cellDiffInfo, metadataChanged);
227
const oldLength = this._items.length;
228
this.clear();
229
this._items.splice(0, oldLength);
230
231
let placeholder: DiffElementPlaceholderViewModel | undefined = undefined;
232
this.originalCellViewModels = cellViewModels;
233
cellViewModels.forEach((vm, index) => {
234
if (vm.type === 'unchanged' && !this.excludeUnchangedPlaceholder) {
235
if (!placeholder) {
236
vm.displayIconToHideUnmodifiedCells = true;
237
placeholder = new DiffElementPlaceholderViewModel(vm.mainDocumentTextModel, vm.editorEventDispatcher, vm.initData);
238
this._items.push(placeholder);
239
const placeholderItem = placeholder;
240
241
this.disposables.add(placeholderItem.onUnfoldHiddenCells(() => {
242
const hiddenCellViewModels = this.placeholderAndRelatedCells.get(placeholderItem);
243
if (!Array.isArray(hiddenCellViewModels)) {
244
return;
245
}
246
const start = this._items.indexOf(placeholderItem);
247
this._items.splice(start, 1, ...hiddenCellViewModels);
248
this._onDidChangeItems.fire({ start, deleteCount: 1, elements: hiddenCellViewModels });
249
}));
250
this.disposables.add(vm.onHideUnchangedCells(() => {
251
const hiddenCellViewModels = this.placeholderAndRelatedCells.get(placeholderItem);
252
if (!Array.isArray(hiddenCellViewModels)) {
253
return;
254
}
255
const start = this._items.indexOf(vm);
256
this._items.splice(start, hiddenCellViewModels.length, placeholderItem);
257
this._onDidChangeItems.fire({ start, deleteCount: hiddenCellViewModels.length, elements: [placeholderItem] });
258
}));
259
}
260
const hiddenCellViewModels = this.placeholderAndRelatedCells.get(placeholder) || [];
261
hiddenCellViewModels.push(vm);
262
this.placeholderAndRelatedCells.set(placeholder, hiddenCellViewModels);
263
placeholder.hiddenCells.push(vm);
264
} else {
265
placeholder = undefined;
266
this._items.push(vm);
267
}
268
});
269
270
// Note, ensure all of the height calculations are done before firing the event.
271
// This is to ensure that the diff editor is not resized multiple times, thereby avoiding flickering.
272
this._onDidChangeItems.fire({ start: 0, deleteCount: oldLength, elements: this._items, firstChangeIndex });
273
}
274
private async createDiffViewModels(computedCellDiffs: CellDiffInfo[], metadataChanged: boolean) {
275
const originalModel = this.model.original.notebook;
276
const modifiedModel = this.model.modified.notebook;
277
const initData = {
278
metadataStatusHeight: this.configurationService.getValue('notebook.diff.ignoreMetadata') ? 0 : 25,
279
outputStatusHeight: this.configurationService.getValue<boolean>('notebook.diff.ignoreOutputs') || !!(modifiedModel.transientOptions.transientOutputs) ? 0 : 25,
280
fontInfo: this.fontInfo
281
};
282
283
const viewModels: (SingleSideDiffElementViewModel | SideBySideDiffElementViewModel | NotebookDocumentMetadataViewModel)[] = [];
284
this.notebookMetadataViewModel = this._register(new NotebookDocumentMetadataViewModel(this.model.original.notebook, this.model.modified.notebook, metadataChanged ? 'modifiedMetadata' : 'unchangedMetadata', this.eventDispatcher, initData, this.notebookService, this.diffEditorHeightCalculator));
285
if (!this.ignoreMetadata) {
286
if (metadataChanged) {
287
await this.notebookMetadataViewModel.computeHeights();
288
}
289
viewModels.push(this.notebookMetadataViewModel);
290
}
291
const cellViewModels = await Promise.all(computedCellDiffs.map(async (diff) => {
292
switch (diff.type) {
293
case 'delete': {
294
return new SingleSideDiffElementViewModel(
295
originalModel,
296
modifiedModel,
297
originalModel.cells[diff.originalCellIndex],
298
undefined,
299
'delete',
300
this.eventDispatcher,
301
initData,
302
this.notebookService,
303
this.configurationService,
304
this.diffEditorHeightCalculator,
305
diff.originalCellIndex
306
);
307
}
308
case 'insert': {
309
return new SingleSideDiffElementViewModel(
310
modifiedModel,
311
originalModel,
312
undefined,
313
modifiedModel.cells[diff.modifiedCellIndex],
314
'insert',
315
this.eventDispatcher,
316
initData,
317
this.notebookService,
318
this.configurationService,
319
this.diffEditorHeightCalculator,
320
diff.modifiedCellIndex
321
);
322
}
323
case 'modified': {
324
const viewModel = new SideBySideDiffElementViewModel(
325
this.model.modified.notebook,
326
this.model.original.notebook,
327
originalModel.cells[diff.originalCellIndex],
328
modifiedModel.cells[diff.modifiedCellIndex],
329
'modified',
330
this.eventDispatcher,
331
initData,
332
this.notebookService,
333
this.configurationService,
334
diff.originalCellIndex,
335
this.diffEditorHeightCalculator
336
);
337
// Reduces flicker (compute this before setting the model)
338
// Else when the model is set, the height of the editor will be x, after diff is computed, then height will be y.
339
// & that results in flicker.
340
await viewModel.computeEditorHeights();
341
return viewModel;
342
}
343
case 'unchanged': {
344
return new SideBySideDiffElementViewModel(
345
this.model.modified.notebook,
346
this.model.original.notebook,
347
originalModel.cells[diff.originalCellIndex],
348
modifiedModel.cells[diff.modifiedCellIndex],
349
'unchanged', this.eventDispatcher,
350
initData,
351
this.notebookService,
352
this.configurationService,
353
diff.originalCellIndex,
354
this.diffEditorHeightCalculator
355
);
356
}
357
}
358
}));
359
360
cellViewModels.forEach(vm => viewModels.push(vm));
361
362
return viewModels;
363
}
364
365
}
366
367
368
/**
369
* making sure that swapping cells are always translated to `insert+delete`.
370
*/
371
export function prettyChanges(original: NotebookTextModel, modified: NotebookTextModel, diffResult: IDiffResult) {
372
const changes = diffResult.changes;
373
for (let i = 0; i < diffResult.changes.length - 1; i++) {
374
// then we know there is another change after current one
375
const curr = changes[i];
376
const next = changes[i + 1];
377
const x = curr.originalStart;
378
const y = curr.modifiedStart;
379
380
if (
381
curr.originalLength === 1
382
&& curr.modifiedLength === 0
383
&& next.originalStart === x + 2
384
&& next.originalLength === 0
385
&& next.modifiedStart === y + 1
386
&& next.modifiedLength === 1
387
&& original.cells[x].getHashValue() === modified.cells[y + 1].getHashValue()
388
&& original.cells[x + 1].getHashValue() === modified.cells[y].getHashValue()
389
) {
390
// this is a swap
391
curr.originalStart = x;
392
curr.originalLength = 0;
393
curr.modifiedStart = y;
394
curr.modifiedLength = 1;
395
396
next.originalStart = x + 1;
397
next.originalLength = 1;
398
next.modifiedStart = y + 2;
399
next.modifiedLength = 0;
400
401
i++;
402
}
403
}
404
}
405
406
export type CellDiffInfo = {
407
originalCellIndex: number;
408
modifiedCellIndex: number;
409
type: 'unchanged' | 'modified';
410
} |
411
{
412
originalCellIndex: number;
413
type: 'delete';
414
} |
415
{
416
modifiedCellIndex: number;
417
type: 'insert';
418
};
419
420
function isEqual(cellDiffInfo: CellDiffInfo[], viewModels: IDiffElementViewModelBase[], model: INotebookDiffEditorModel) {
421
if (cellDiffInfo.length !== viewModels.length) {
422
return false;
423
}
424
const originalModel = model.original.notebook;
425
const modifiedModel = model.modified.notebook;
426
for (let i = 0; i < viewModels.length; i++) {
427
const a = cellDiffInfo[i];
428
const b = viewModels[i];
429
if (a.type !== b.type) {
430
return false;
431
}
432
switch (a.type) {
433
case 'delete': {
434
if (originalModel.cells[a.originalCellIndex].handle !== b.original?.handle) {
435
return false;
436
}
437
continue;
438
}
439
case 'insert': {
440
if (modifiedModel.cells[a.modifiedCellIndex].handle !== b.modified?.handle) {
441
return false;
442
}
443
continue;
444
}
445
default: {
446
if (originalModel.cells[a.originalCellIndex].handle !== b.original?.handle) {
447
return false;
448
}
449
if (modifiedModel.cells[a.modifiedCellIndex].handle !== b.modified?.handle) {
450
return false;
451
}
452
continue;
453
}
454
}
455
}
456
457
return true;
458
}
459
export abstract class NotebookMultiDiffEditorItem extends MultiDiffEditorItem {
460
constructor(
461
originalUri: URI | undefined,
462
modifiedUri: URI | undefined,
463
goToFileUri: URI | undefined,
464
public readonly type: IDiffElementViewModelBase['type'],
465
public readonly containerType: IDiffElementViewModelBase['type'],
466
public kind: 'Cell' | 'Metadata' | 'Output',
467
contextKeys?: Record<string, ContextKeyValue>,
468
) {
469
super(originalUri, modifiedUri, goToFileUri, undefined, contextKeys);
470
}
471
}
472
473
class NotebookMultiDiffEditorCellItem extends NotebookMultiDiffEditorItem {
474
constructor(
475
originalUri: URI | undefined,
476
modifiedUri: URI | undefined,
477
type: IDiffElementViewModelBase['type'],
478
containerType: IDiffElementViewModelBase['type'],
479
) {
480
super(originalUri, modifiedUri, modifiedUri || originalUri, type, containerType, 'Cell', {
481
[NOTEBOOK_DIFF_ITEM_KIND.key]: 'Cell',
482
[NOTEBOOK_DIFF_ITEM_DIFF_STATE.key]: type
483
});
484
}
485
}
486
487
class NotebookMultiDiffEditorMetadataItem extends NotebookMultiDiffEditorItem {
488
constructor(
489
originalUri: URI | undefined,
490
modifiedUri: URI | undefined,
491
type: IDiffElementViewModelBase['type'],
492
containerType: IDiffElementViewModelBase['type'],
493
) {
494
super(originalUri, modifiedUri, modifiedUri || originalUri, type, containerType, 'Metadata', {
495
[NOTEBOOK_DIFF_ITEM_KIND.key]: 'Metadata',
496
[NOTEBOOK_DIFF_ITEM_DIFF_STATE.key]: type
497
});
498
}
499
}
500
501
class NotebookMultiDiffEditorOutputItem extends NotebookMultiDiffEditorItem {
502
constructor(
503
originalUri: URI | undefined,
504
modifiedUri: URI | undefined,
505
type: IDiffElementViewModelBase['type'],
506
containerType: IDiffElementViewModelBase['type'],
507
) {
508
super(originalUri, modifiedUri, modifiedUri || originalUri, type, containerType, 'Output', {
509
[NOTEBOOK_DIFF_ITEM_KIND.key]: 'Output',
510
[NOTEBOOK_DIFF_ITEM_DIFF_STATE.key]: type
511
});
512
}
513
}
514
515