mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-30 18:46:00 +01:00
459 lines
21 KiB
TypeScript
459 lines
21 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import assert from 'assert';
|
|
import sinon from 'sinon';
|
|
import { Dimension } from '../../../../base/browser/dom.js';
|
|
import { Button } from '../../../../base/browser/ui/button/button.js';
|
|
import { Event, ValueWithChangeEvent } from '../../../../base/common/event.js';
|
|
import { autorun, waitForState } from '../../../../base/common/observable.js';
|
|
import { URI } from '../../../../base/common/uri.js';
|
|
import { mock } from '../../../../base/test/common/mock.js';
|
|
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
|
|
import { AccessibilitySupport } from '../../../../platform/accessibility/common/accessibility.js';
|
|
import { IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
|
|
import { IActionViewItemService, NullActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
|
|
import { IMenu, IMenuService } from '../../../../platform/actions/common/actions.js';
|
|
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
|
|
import { emptyProgressRunner, IEditorProgressService } from '../../../../platform/progress/common/progress.js';
|
|
import { InMemoryStorageService, IStorageService } from '../../../../platform/storage/common/storage.js';
|
|
import { IDiffProviderFactoryService } from '../../../browser/widget/diffEditor/diffProviderFactoryService.js';
|
|
import { DiffEditorWidget } from '../../../browser/widget/diffEditor/diffEditorWidget.js';
|
|
import { RefCounted } from '../../../browser/widget/diffEditor/utils.js';
|
|
import { DiffItemSource, IDocumentDiffItem, IMultiDiffEditorModel } from '../../../browser/widget/multiDiffEditor/model.js';
|
|
import { getMultiDiffEditorVariantConfiguration, MultiDiffEditorVariant } from '../../../browser/widget/multiDiffEditor/multiDiffEditorOptions.js';
|
|
import { MultiDiffEditorWidget } from '../../../browser/widget/multiDiffEditor/multiDiffEditorWidget.js';
|
|
import { IWorkbenchUIElementFactory } from '../../../browser/widget/multiDiffEditor/workbenchUIElementFactory.js';
|
|
import { EditorOption } from '../../../common/config/editorOptions.js';
|
|
import { IDocumentDiff, IDocumentDiffProvider } from '../../../common/diff/documentDiffProvider.js';
|
|
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
|
|
import { instantiateTextModel } from '../../common/testTextModel.js';
|
|
import { TestDiffProviderFactoryService } from '../diff/testDiffProviderFactoryService.js';
|
|
import { createCodeEditorServices } from '../testCodeEditor.js';
|
|
|
|
suite('MultiDiffEditorWidget', () => {
|
|
|
|
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
|
|
|
|
teardown(() => {
|
|
sinon.restore();
|
|
});
|
|
|
|
test('uses closed variant configurations', () => {
|
|
assert.deepStrictEqual({
|
|
standard: getMultiDiffEditorVariantConfiguration(MultiDiffEditorVariant.Standard),
|
|
compact: getMultiDiffEditorVariantConfiguration(MultiDiffEditorVariant.Compact),
|
|
}, {
|
|
standard: {
|
|
className: 'multiDiffEditor-standard',
|
|
horizontalInsets: { left: 9, right: 9 },
|
|
headerHeight: 40,
|
|
contentBottomPadding: 0,
|
|
headerClickToCollapse: false,
|
|
},
|
|
compact: {
|
|
className: 'multiDiffEditor-compact',
|
|
horizontalInsets: { left: 0, right: 0 },
|
|
headerHeight: 32,
|
|
contentBottomPadding: 8,
|
|
headerClickToCollapse: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
test('models bottom padding as trailing scroll content', () => {
|
|
const services = new ServiceCollection();
|
|
services.set(IAccessibilitySignalService, new class extends mock<IAccessibilitySignalService>() { }());
|
|
services.set(IActionViewItemService, new NullActionViewItemService());
|
|
services.set(IEditorProgressService, new class extends mock<IEditorProgressService>() { }());
|
|
services.set(IDiffProviderFactoryService, new TestDiffProviderFactoryService());
|
|
services.set(IStorageService, disposables.add(new InMemoryStorageService()));
|
|
services.set(IMenuService, new class extends mock<IMenuService>() {
|
|
override createMenu(): IMenu {
|
|
return new class extends mock<IMenu>() {
|
|
override readonly onDidChange = Event.None;
|
|
override getActions() { return []; }
|
|
override dispose(): void { }
|
|
}();
|
|
}
|
|
}());
|
|
const instantiationService = createCodeEditorServices(disposables, services);
|
|
const container = document.createElement('div');
|
|
const widget = instantiationService.createInstance(
|
|
MultiDiffEditorWidget,
|
|
container,
|
|
{} satisfies IWorkbenchUIElementFactory,
|
|
{ variant: MultiDiffEditorVariant.Standard },
|
|
);
|
|
widget.layout(new Dimension(800, 200));
|
|
const initialState = widget.getLayoutDebugState().get();
|
|
widget.setPaddingBottom(24);
|
|
|
|
try {
|
|
const state = widget.getLayoutDebugState().get();
|
|
assert.deepStrictEqual({
|
|
logicalScrollHeightDelta: state.layout.logicalScrollHeight - initialState.layout.logicalScrollHeight,
|
|
scrollHeightDelta: state.scrollDimensions.scrollHeight - initialState.scrollDimensions.scrollHeight,
|
|
diffItems: state.items.length,
|
|
}, {
|
|
logicalScrollHeightDelta: 24,
|
|
scrollHeightDelta: 24,
|
|
diffItems: 0,
|
|
});
|
|
} finally {
|
|
widget.dispose();
|
|
}
|
|
});
|
|
|
|
test('renders binary files as a placeholder', async () => {
|
|
const services = new ServiceCollection();
|
|
services.set(IAccessibilitySignalService, new class extends mock<IAccessibilitySignalService>() { }());
|
|
services.set(IActionViewItemService, new NullActionViewItemService());
|
|
services.set(IEditorProgressService, new class extends mock<IEditorProgressService>() { }());
|
|
services.set(IDiffProviderFactoryService, new TestDiffProviderFactoryService());
|
|
services.set(IStorageService, disposables.add(new InMemoryStorageService()));
|
|
services.set(IMenuService, new class extends mock<IMenuService>() {
|
|
override createMenu(): IMenu {
|
|
return new class extends mock<IMenu>() {
|
|
override readonly onDidChange = Event.None;
|
|
override getActions() { return []; }
|
|
override dispose(): void { }
|
|
}();
|
|
}
|
|
}());
|
|
const instantiationService = createCodeEditorServices(disposables, services);
|
|
const originalUri = URI.parse('inmemory://original/image.png');
|
|
const modifiedUri = URI.parse('inmemory://modified/image.png');
|
|
const documentItem = RefCounted.createOfNonDisposable<IDocumentDiffItem>({
|
|
original: new DiffItemSource(originalUri, undefined),
|
|
modified: new DiffItemSource(modifiedUri, undefined),
|
|
}, { dispose() { } });
|
|
const model: IMultiDiffEditorModel = {
|
|
documents: ValueWithChangeEvent.const([documentItem]),
|
|
};
|
|
let openedDiff: { original: URI; modified: URI } | undefined;
|
|
const container = document.createElement('div');
|
|
const widget = instantiationService.createInstance(
|
|
MultiDiffEditorWidget,
|
|
container,
|
|
{
|
|
openDiffEditor: (original, modified) => openedDiff = { original, modified },
|
|
} satisfies IWorkbenchUIElementFactory,
|
|
{ variant: MultiDiffEditorVariant.Standard },
|
|
);
|
|
widget.layout(new Dimension(800, 600));
|
|
const viewModel = widget.createViewModel(model);
|
|
await waitForState(viewModel.items, items => items.length === 1);
|
|
widget.setViewModel(viewModel);
|
|
widget.reveal({ original: originalUri, modified: modifiedUri }, { highlight: false });
|
|
await waitForState(widget.getLayoutDebugState(), state => state.items[0]?.hasTemplate === true);
|
|
|
|
try {
|
|
const placeholder = widget.getRootElement().querySelector<HTMLElement>('.binary-file-placeholder');
|
|
const editor = widget.getRootElement().querySelector<HTMLElement>('.editorContainer');
|
|
const openDiffButton = placeholder?.querySelector<HTMLElement>('.monaco-button');
|
|
const focusSpy = sinon.spy(Button.prototype, 'focus');
|
|
const canFocusActiveItem = widget.focus();
|
|
openDiffButton?.click();
|
|
assert.deepStrictEqual({
|
|
text: placeholder?.textContent,
|
|
display: placeholder?.style.display,
|
|
tabIndex: placeholder?.tabIndex,
|
|
role: placeholder?.getAttribute('role'),
|
|
ariaLabel: placeholder?.getAttribute('aria-label'),
|
|
openDiffButtonText: openDiffButton?.textContent,
|
|
openDiffButtonSecondary: openDiffButton?.classList.contains('secondary'),
|
|
openDiffButtonFocused: focusSpy.calledOnce,
|
|
openedOriginalUri: openedDiff?.original.toString(),
|
|
openedModifiedUri: openedDiff?.modified.toString(),
|
|
editorDisplay: editor?.style.display,
|
|
itemHeight: widget.getLayoutDebugState().get().items[0].verticalState.contentHeight,
|
|
canFocusActiveItem,
|
|
findsDocumentItem: widget.findDocumentDiffItem(modifiedUri) === documentItem.object,
|
|
hasCodeEditorForBinaryResource: widget.tryGetCodeEditor(modifiedUri) !== undefined,
|
|
}, {
|
|
text: 'Binary file changedOpen Diff',
|
|
display: 'grid',
|
|
tabIndex: -1,
|
|
role: 'group',
|
|
ariaLabel: 'Binary file changed',
|
|
openDiffButtonText: 'Open Diff',
|
|
openDiffButtonSecondary: true,
|
|
openDiffButtonFocused: true,
|
|
openedOriginalUri: originalUri.toString(),
|
|
openedModifiedUri: modifiedUri.toString(),
|
|
editorDisplay: 'none',
|
|
itemHeight: 140,
|
|
canFocusActiveItem: true,
|
|
findsDocumentItem: true,
|
|
hasCodeEditorForBinaryResource: false,
|
|
});
|
|
} finally {
|
|
widget.setViewModel(undefined);
|
|
viewModel.dispose();
|
|
widget.dispose();
|
|
documentItem.dispose();
|
|
}
|
|
});
|
|
|
|
test('applies document and responsive layout options before attaching the diff model', async () => {
|
|
const services = new ServiceCollection();
|
|
services.set(IAccessibilitySignalService, new class extends mock<IAccessibilitySignalService>() { }());
|
|
services.set(IActionViewItemService, new NullActionViewItemService());
|
|
services.set(IEditorProgressService, new class extends mock<IEditorProgressService>() { }());
|
|
services.set(IDiffProviderFactoryService, new TestDiffProviderFactoryService());
|
|
services.set(IStorageService, disposables.add(new InMemoryStorageService()));
|
|
services.set(IMenuService, new class extends mock<IMenuService>() {
|
|
override createMenu(): IMenu {
|
|
return new class extends mock<IMenu>() {
|
|
override readonly onDidChange = Event.None;
|
|
override getActions() { return []; }
|
|
override dispose(): void { }
|
|
}();
|
|
}
|
|
}());
|
|
const instantiationService = createCodeEditorServices(disposables, services);
|
|
|
|
const originalUri = URI.parse('inmemory://original/test.js');
|
|
const modifiedUri = URI.parse('inmemory://modified/test.js');
|
|
const original = disposables.add(instantiateTextModel(instantiationService, 'const value = 1;', undefined, undefined, originalUri));
|
|
const modified = disposables.add(instantiateTextModel(instantiationService, 'const value = 2;', undefined, undefined, modifiedUri));
|
|
const documentItem = RefCounted.createOfNonDisposable<IDocumentDiffItem>({
|
|
original: new DiffItemSource(originalUri, original),
|
|
modified: new DiffItemSource(modifiedUri, modified),
|
|
options: { accessibilitySupport: 'off' },
|
|
}, { dispose() { } });
|
|
const model: IMultiDiffEditorModel = {
|
|
documents: ValueWithChangeEvent.const([documentItem]),
|
|
};
|
|
|
|
const updateOptionsSpy = sinon.spy(DiffEditorWidget.prototype, 'updateOptions');
|
|
const setDiffModelSpy = sinon.spy(DiffEditorWidget.prototype, 'setDiffModel');
|
|
|
|
const container = document.createElement('div');
|
|
const widget = instantiationService.createInstance(
|
|
MultiDiffEditorWidget,
|
|
container,
|
|
{} satisfies IWorkbenchUIElementFactory,
|
|
{ variant: MultiDiffEditorVariant.Standard },
|
|
);
|
|
widget.setViewMode('automatic');
|
|
widget.layout(new Dimension(800, 600));
|
|
const viewModel = widget.createViewModel(model);
|
|
await waitForState(viewModel.items, items => items.length === 1);
|
|
widget.setViewModel(viewModel);
|
|
widget.reveal({ original: originalUri, modified: modifiedUri }, { highlight: false });
|
|
|
|
try {
|
|
const activeControl = widget.getActiveControl();
|
|
const renderSideBySideWhenNarrow = activeControl?.renderSideBySide;
|
|
const automaticLayoutWhenNarrow = widget.getContextKeyService().getContextKeyValue(EditorContextKeys.diffEditorAutomaticRenderSideBySide.key);
|
|
widget.layout(new Dimension(1000, 600));
|
|
assert.deepStrictEqual({
|
|
configuredAccessibilitySupport: updateOptionsSpy.firstCall.args[0].accessibilitySupport,
|
|
configuredRenderSideBySide: updateOptionsSpy.firstCall.args[0].renderSideBySide,
|
|
configuredUseInlineViewWhenSpaceIsLimited: updateOptionsSpy.firstCall.args[0].useInlineViewWhenSpaceIsLimited,
|
|
renderSideBySideWhenNarrow,
|
|
renderSideBySideWhenWide: activeControl?.renderSideBySide,
|
|
automaticLayoutWhenNarrow,
|
|
automaticLayoutWhenWide: widget.getContextKeyService().getContextKeyValue(EditorContextKeys.diffEditorAutomaticRenderSideBySide.key),
|
|
optionsAppliedBeforeModel: updateOptionsSpy.calledBefore(setDiffModelSpy),
|
|
effectiveAccessibilitySupport: activeControl?.getModifiedEditor().getOption(EditorOption.accessibilitySupport),
|
|
}, {
|
|
configuredAccessibilitySupport: 'off',
|
|
configuredRenderSideBySide: true,
|
|
configuredUseInlineViewWhenSpaceIsLimited: true,
|
|
renderSideBySideWhenNarrow: false,
|
|
renderSideBySideWhenWide: true,
|
|
automaticLayoutWhenNarrow: false,
|
|
automaticLayoutWhenWide: true,
|
|
optionsAppliedBeforeModel: true,
|
|
effectiveAccessibilitySupport: AccessibilitySupport.Disabled,
|
|
});
|
|
} finally {
|
|
widget.setViewModel(undefined);
|
|
viewModel.dispose();
|
|
widget.dispose();
|
|
documentItem.dispose();
|
|
}
|
|
});
|
|
|
|
test('uses the taller side while binding a deleted file', async () => {
|
|
const services = new ServiceCollection();
|
|
services.set(IAccessibilitySignalService, new class extends mock<IAccessibilitySignalService>() { }());
|
|
services.set(IActionViewItemService, new NullActionViewItemService());
|
|
services.set(IEditorProgressService, new class extends mock<IEditorProgressService>() { }());
|
|
services.set(IDiffProviderFactoryService, new TestDiffProviderFactoryService());
|
|
services.set(IStorageService, disposables.add(new InMemoryStorageService()));
|
|
services.set(IMenuService, new class extends mock<IMenuService>() {
|
|
override createMenu(): IMenu {
|
|
return new class extends mock<IMenu>() {
|
|
override readonly onDidChange = Event.None;
|
|
override getActions() { return []; }
|
|
override dispose(): void { }
|
|
}();
|
|
}
|
|
}());
|
|
const instantiationService = createCodeEditorServices(disposables, services);
|
|
|
|
const originalUri = URI.parse('inmemory://original/deleted.js');
|
|
const originalContent = Array.from({ length: 64 }, (_, index) => `line ${index}`).join('\n');
|
|
const original = disposables.add(instantiateTextModel(instantiationService, originalContent, undefined, undefined, originalUri));
|
|
const documentItem = RefCounted.createOfNonDisposable<IDocumentDiffItem>({
|
|
original: new DiffItemSource(originalUri, original),
|
|
modified: undefined,
|
|
options: { accessibilitySupport: 'off' },
|
|
}, { dispose() { } });
|
|
const model: IMultiDiffEditorModel = {
|
|
documents: ValueWithChangeEvent.const([documentItem]),
|
|
};
|
|
|
|
const container = document.createElement('div');
|
|
const widget = instantiationService.createInstance(
|
|
MultiDiffEditorWidget,
|
|
container,
|
|
{} satisfies IWorkbenchUIElementFactory,
|
|
{ variant: MultiDiffEditorVariant.Standard },
|
|
);
|
|
widget.layout(new Dimension(800, 600));
|
|
const viewModel = widget.createViewModel(model);
|
|
await waitForState(viewModel.items, items => items.length === 1);
|
|
await waitForState(viewModel.items.get()[0].diffEditorViewModel.isDiffUpToDate, value => value);
|
|
|
|
const observedHeights: number[] = [];
|
|
const observer = autorun(reader => {
|
|
const item = widget.getLayoutDebugState().read(reader).items[0];
|
|
if (item?.hasTemplate) {
|
|
observedHeights.push(item.verticalState.contentHeight);
|
|
}
|
|
});
|
|
try {
|
|
widget.setViewModel(viewModel);
|
|
widget.reveal({ original: originalUri, modified: undefined }, { highlight: false });
|
|
await waitForState(widget.getLayoutDebugState(), state => state.items[0]?.hasTemplate);
|
|
const item = widget.getLayoutDebugState().get().items[0];
|
|
const expectedHeight = widget.getActiveControl()!.getOriginalEditor().getContentHeight() + 40;
|
|
|
|
assert.deepStrictEqual({
|
|
minimumObservedHeight: Math.min(...observedHeights),
|
|
finalHeight: item.verticalState.contentHeight,
|
|
}, {
|
|
minimumObservedHeight: expectedHeight,
|
|
finalHeight: expectedHeight,
|
|
});
|
|
} finally {
|
|
observer.dispose();
|
|
widget.setViewModel(undefined);
|
|
viewModel.dispose();
|
|
widget.dispose();
|
|
documentItem.dispose();
|
|
}
|
|
});
|
|
|
|
test('preserves expanded height when a collapsed template is recycled', async () => {
|
|
const services = new ServiceCollection();
|
|
services.set(IAccessibilitySignalService, new class extends mock<IAccessibilitySignalService>() { }());
|
|
services.set(IActionViewItemService, new NullActionViewItemService());
|
|
services.set(IEditorProgressService, new class extends mock<IEditorProgressService>() {
|
|
override show() { return emptyProgressRunner; }
|
|
}());
|
|
services.set(IDiffProviderFactoryService, new PendingDiffProviderFactoryService());
|
|
services.set(IStorageService, disposables.add(new InMemoryStorageService()));
|
|
services.set(IMenuService, new class extends mock<IMenuService>() {
|
|
override createMenu(): IMenu {
|
|
return new class extends mock<IMenu>() {
|
|
override readonly onDidChange = Event.None;
|
|
override getActions() { return []; }
|
|
override dispose(): void { }
|
|
}();
|
|
}
|
|
}());
|
|
const instantiationService = createCodeEditorServices(disposables, services);
|
|
const documentItems: RefCounted<IDocumentDiffItem>[] = [];
|
|
const originalUris: URI[] = [];
|
|
const modifiedUris: URI[] = [];
|
|
for (let index = 0; index < 3; index++) {
|
|
const originalUri = URI.parse(`inmemory://original/test-${index}.js`);
|
|
const modifiedUri = URI.parse(`inmemory://modified/test-${index}.js`);
|
|
const original = disposables.add(instantiateTextModel(instantiationService, '', undefined, undefined, originalUri));
|
|
const modified = disposables.add(instantiateTextModel(instantiationService, 'const value = 1;', undefined, undefined, modifiedUri));
|
|
documentItems.push(RefCounted.createOfNonDisposable<IDocumentDiffItem>({
|
|
original: new DiffItemSource(originalUri, original),
|
|
modified: new DiffItemSource(modifiedUri, modified),
|
|
options: { accessibilitySupport: 'off' },
|
|
}, { dispose() { } }));
|
|
originalUris.push(originalUri);
|
|
modifiedUris.push(modifiedUri);
|
|
}
|
|
const model: IMultiDiffEditorModel = {
|
|
documents: ValueWithChangeEvent.const(documentItems),
|
|
};
|
|
const container = document.createElement('div');
|
|
const widget = instantiationService.createInstance(
|
|
MultiDiffEditorWidget,
|
|
container,
|
|
{} satisfies IWorkbenchUIElementFactory,
|
|
{ variant: MultiDiffEditorVariant.Standard },
|
|
);
|
|
widget.layout(new Dimension(800, 200));
|
|
const viewModel = widget.createViewModel(model);
|
|
await waitForState(viewModel.items, items => items.length === documentItems.length);
|
|
viewModel.items.get()[0].lastTemplateData.set({ expandedContentHeight: 2000, selections: undefined }, undefined);
|
|
widget.setViewModel(viewModel);
|
|
widget.reveal({ original: originalUris[0], modified: modifiedUris[0] }, { highlight: false });
|
|
|
|
const observedLastItemHeights: number[] = [];
|
|
const observer = autorun(reader => {
|
|
const lastItem = widget.getLayoutDebugState().read(reader).items.at(-1);
|
|
if (lastItem?.hasTemplate) {
|
|
observedLastItemHeights.push(lastItem.verticalState.contentHeight);
|
|
}
|
|
});
|
|
try {
|
|
const firstItem = widget.getLayoutDebugState().get().items[0];
|
|
viewModel.items.get()[0].collapsed.set(true, undefined);
|
|
widget.reveal({ original: originalUris.at(-1), modified: modifiedUris.at(-1) }, { highlight: false });
|
|
const cachedFirstItemHeight = viewModel.items.get()[0].lastTemplateData.get().expandedContentHeight;
|
|
widget.reveal({ original: originalUris[0], modified: modifiedUris[0] }, { highlight: false });
|
|
viewModel.items.get()[0].collapsed.set(false, undefined);
|
|
|
|
assert.deepStrictEqual({
|
|
firstItemHasTemplate: firstItem.hasTemplate,
|
|
firstItemHeight: firstItem.verticalState.contentHeight,
|
|
cachedFirstItemHeight,
|
|
firstObservedLastItemHeight: observedLastItemHeights[0],
|
|
restoredFirstItemHeight: widget.getLayoutDebugState().get().items[0].verticalState.contentHeight,
|
|
}, {
|
|
firstItemHasTemplate: true,
|
|
firstItemHeight: 2000,
|
|
cachedFirstItemHeight: 2000,
|
|
firstObservedLastItemHeight: 500,
|
|
restoredFirstItemHeight: 2000,
|
|
});
|
|
} finally {
|
|
observer.dispose();
|
|
widget.setViewModel(undefined);
|
|
viewModel.dispose();
|
|
widget.dispose();
|
|
for (const documentItem of documentItems) {
|
|
documentItem.dispose();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
class PendingDiffProviderFactoryService implements IDiffProviderFactoryService {
|
|
declare readonly _serviceBrand: undefined;
|
|
|
|
createDiffProvider(): IDocumentDiffProvider {
|
|
return new class extends mock<IDocumentDiffProvider>() {
|
|
override readonly onDidChange = Event.None;
|
|
override computeDiff(): Promise<IDocumentDiff> {
|
|
return new Promise(() => { });
|
|
}
|
|
}();
|
|
}
|
|
}
|