Коммит 85492c33 создал по автору Oleg Solomko's avatar Oleg Solomko
Просмотр файлов

fix class field intialization issues, II of front matter decorator

владелец b3a3f082
......@@ -9,7 +9,7 @@ import { assertDefined } from '../../../../../base/common/types.js';
import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js';
import { assert, assertNever } from '../../../../../base/common/assert.js';
import { CarriageReturn } from '../../linesCodec/tokens/carriageReturn.js';
import { FrontMatterHeaderToken } from '../tokens/frontMatterHeaderToken.js';
import { FrontMatterHeader } from '../tokens/frontMatterHeader.js';
import { FrontMatterMarker, TMarkerToken } from '../tokens/frontMatterMarker.js';
import { assertNotConsumed, IAcceptTokenSuccess, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js';
......@@ -96,7 +96,7 @@ export class PartialFrontMatterStartMarker extends ParserBase<TMarkerToken, Part
* Parses a Front Matter header that already has a start marker
* and possibly some content that follows.
*/
export class PartialFrontMatterHeader extends ParserBase<TSimpleDecoderToken, PartialFrontMatterHeader | FrontMatterHeaderToken> {
export class PartialFrontMatterHeader extends ParserBase<TSimpleDecoderToken, PartialFrontMatterHeader | FrontMatterHeader> {
/**
* Parser instance for the end marker of the Front Matter header.
*/
......@@ -121,12 +121,12 @@ export class PartialFrontMatterHeader extends ParserBase<TSimpleDecoderToken, Pa
}
/**
* Convert the current token sequence into a {@link FrontMatterHeaderToken} token.
* Convert the current token sequence into a {@link FrontMatterHeader} token.
*
* Note! that this method marks the current parser object as "consumed"
* hence it should not be used after this method is called.
*/
public asFrontMatterHeader(): FrontMatterHeaderToken | null {
public asFrontMatterHeader(): FrontMatterHeader | null {
assertDefined(
this.maybeEndMarker,
'Cannot convert to Front Matter header token without an end marker.',
......@@ -142,7 +142,7 @@ export class PartialFrontMatterHeader extends ParserBase<TSimpleDecoderToken, Pa
this.isConsumed = true;
return FrontMatterHeaderToken.fromTokens(
return FrontMatterHeader.fromTokens(
this.startMarker.tokens,
this.currentTokens,
this.maybeEndMarker.tokens,
......@@ -150,7 +150,7 @@ export class PartialFrontMatterHeader extends ParserBase<TSimpleDecoderToken, Pa
}
@assertNotConsumed
public accept(token: TSimpleDecoderToken): TAcceptTokenResult<PartialFrontMatterHeader | FrontMatterHeaderToken> {
public accept(token: TSimpleDecoderToken): TAcceptTokenResult<PartialFrontMatterHeader | FrontMatterHeader> {
// if in the mode of parsing the end marker sequence, forward
// the token to the current end marker parser instance
if (this.maybeEndMarker !== undefined) {
......@@ -189,7 +189,7 @@ export class PartialFrontMatterHeader extends ParserBase<TSimpleDecoderToken, Pa
*/
private acceptEndMarkerToken(
token: TSimpleDecoderToken,
): TAcceptTokenResult<PartialFrontMatterHeader | FrontMatterHeaderToken> {
): TAcceptTokenResult<PartialFrontMatterHeader | FrontMatterHeader> {
assertDefined(
this.maybeEndMarker,
`Partial end marker parser must be initialized.`,
......@@ -228,7 +228,7 @@ export class PartialFrontMatterHeader extends ParserBase<TSimpleDecoderToken, Pa
return {
result: 'success',
wasTokenConsumed: true,
nextParser: FrontMatterHeaderToken.fromTokens(
nextParser: FrontMatterHeader.fromTokens(
this.startMarker.tokens,
this.currentTokens,
this.maybeEndMarker.tokens,
......
......@@ -12,7 +12,7 @@ import { FrontMatterMarker, TMarkerToken } from './frontMatterMarker.js';
/**
* Token that represents a `Front Matter` header in a text.
*/
export class FrontMatterHeaderToken extends MarkdownExtensionsToken {
export class FrontMatterHeader extends MarkdownExtensionsToken {
constructor(
range: Range,
public readonly startMarker: FrontMatterMarker,
......@@ -50,7 +50,7 @@ export class FrontMatterHeaderToken extends MarkdownExtensionsToken {
return false;
}
if (!(other instanceof FrontMatterHeaderToken)) {
if (!(other instanceof FrontMatterHeader)) {
return false;
}
......@@ -68,8 +68,8 @@ export class FrontMatterHeaderToken extends MarkdownExtensionsToken {
startMarkerTokens: readonly TMarkerToken[],
contentTokens: readonly TSimpleDecoderToken[],
endMarkerTokens: readonly TMarkerToken[],
): FrontMatterHeaderToken {
return new FrontMatterHeaderToken(
): FrontMatterHeader {
return new FrontMatterHeader(
BaseToken.fullRange([...startMarkerTokens, ...endMarkerTokens]),
FrontMatterMarker.fromTokens(startMarkerTokens),
Text.fromTokens(contentTokens),
......
......@@ -124,5 +124,7 @@ const toMarker = (
* classes for each specific editor text model.
*/
export class PromptLinkDiagnosticsInstanceManager extends ProviderInstanceManagerBase<PromptLinkDiagnosticsProvider> {
protected override readonly InstanceClass = PromptLinkDiagnosticsProvider;
protected override get InstanceClass() {
return PromptLinkDiagnosticsProvider;
}
}
......@@ -13,6 +13,7 @@ import { IDiffEditor, IEditor } from '../../../../../../../editor/common/editorC
import { IEditorService } from '../../../../../../services/editor/common/editorService.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js';
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js';
import { assertDefined } from '../../../../../../../base/common/types.js';
/**
* Type for a text editor that is used for reusable prompt files.
......@@ -34,7 +35,7 @@ export abstract class ProviderInstanceManagerBase<TInstance extends ProviderInst
/**
* Class object of the managed {@link TInstance}.
*/
protected abstract readonly InstanceClass: new (editor: IPromptFileEditor, ...args: any[]) => TInstance;
protected abstract get InstanceClass(): new (editor: IPromptFileEditor, ...args: any[]) => TInstance;
constructor(
@IEditorService editorService: IEditorService,
......@@ -46,6 +47,13 @@ export abstract class ProviderInstanceManagerBase<TInstance extends ProviderInst
// cache of managed instances
this.instances = this._register(
new ObjectCache((editor: IPromptFileEditor) => {
// sanity check - the new TS/JS discrepancies regarding fields initialization
// logic mean that this can be `undefined` during runtime while defined in TS
assertDefined(
this.InstanceClass,
'Instance class field must be defined.',
);
const instance: TInstance = initService.createInstance(
this.InstanceClass,
editor,
......
......@@ -4,24 +4,224 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from '../../../../../../../nls.js';
import { IPromptsService } from '../../service/types.js';
import { ProviderInstanceBase } from './providerInstanceBase.js';
import { chatSlashCommandBackground } from '../../../chatColors.js';
import { Color, RGBA } from '../../../../../../../base/common/color.js';
import { IRange } from '../../../../../../../editor/common/core/range.js';
import { ProviderInstanceManagerBase } from './providerInstanceManagerBase.js';
import { assertNever } from '../../../../../../../base/common/assert.js';
import { toDisposable } from '../../../../../../../base/common/lifecycle.js';
import { Position } from '../../../../../../../editor/common/core/position.js';
import { Range, IRange } from '../../../../../../../editor/common/core/range.js';
import { IMarkdownString } from '../../../../../../../base/common/htmlContent.js';
import { BaseToken } from '../../../../../../../editor/common/codecs/baseToken.js';
import { TrackedRangeStickiness } from '../../../../../../../editor/common/model.js';
import { ModelDecorationOptions } from '../../../../../../../editor/common/model/textModel.js';
import { IMarkdownString, MarkdownString } from '../../../../../../../base/common/htmlContent.js';
import { IPromptFileEditor, ProviderInstanceManagerBase } from './providerInstanceManagerBase.js';
import { contrastBorder, registerColor } from '../../../../../../../platform/theme/common/colorRegistry.js';
import { IModelDecorationsChangeAccessor, TrackedRangeStickiness } from '../../../../../../../editor/common/model.js';
import { FrontMatterHeader } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from '../../../../../../../platform/theme/common/themeService.js';
import { FrontMatterHeaderToken } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeaderToken.js';
/**
* TODO: @legomushroom - list
* - add active/inactive logic for front matter header
*/
/**
* TODO: @legomushroom
*/
abstract class Decoration<TPromptToken extends BaseToken, TCssClassName extends string = string> {
/**
* TODO: @legomushroom
*/
protected abstract get description(): string;
/**
* TODO: @legomushroom
*/
protected abstract get className(): TCssClassName;
/**
* TODO: @legomushroom
*/
protected abstract get inlineClassName(): TCssClassName;
/**
* TODO: @legomushroom
*/
protected get isWholeLine(): boolean {
return false;
}
/**
* TODO: @legomushroom
*/
protected get hoverMessage(): IMarkdownString | IMarkdownString[] | null {
return null;
}
/**
* TODO: @legomushroom
*/
public readonly id: string;
constructor(
accessor: Pick<IModelDecorationsChangeAccessor, 'addDecoration'>,
protected readonly token: TPromptToken,
) {
this.id = accessor.addDecoration(this.range, this.decorationOptions);
}
/**
* Range of the decoration.
*/
public get range(): Range {
return this.token.range;
}
/**
* TODO: @legomushroom
*/
public render(
accessor: Pick<IModelDecorationsChangeAccessor, 'changeDecoration' | 'changeDecorationOptions'>,
): this {
accessor.changeDecorationOptions(
this.id,
this.decorationOptions,
);
return this;
}
/**
* TODO: @legomushroom
*/
protected get decorationOptions(): ModelDecorationOptions {
return ModelDecorationOptions.createDynamic({
description: this.description,
hoverMessage: this.hoverMessage,
className: this.className,
inlineClassName: this.inlineClassName,
isWholeLine: this.isWholeLine,
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
shouldFillLineOnLineBreak: true,
});
}
}
/**
* TODO: @legomushroom
*/
abstract class ReactiveDecoration<TPromptToken extends BaseToken, TCssClassName extends string = string> extends Decoration<TPromptToken, TCssClassName> {
/**
* Whether the decoration has changed since the last {@link render}.
*/
public get changed(): boolean {
return this.didChange;
}
/**
* TODO: @legomushroom
*/
private cursorPosition?: Position | null;
/**
* Private field for the {@link changed} property.
*/
private didChange = true;
constructor(
accessor: Pick<IModelDecorationsChangeAccessor, 'addDecoration'>,
token: TPromptToken,
) {
super(accessor, token);
}
/**
* Whether cursor is currently inside the decoration range.
*/
protected get active(): boolean {
if (!this.cursorPosition) {
return false;
}
// when cursor is at the end of a range, the range considered to
// not contain the position, but we want to include it
const atEnd = (this.range.endLineNumber === this.cursorPosition.lineNumber)
&& (this.range.endColumn === this.cursorPosition.column);
return atEnd || this.range.containsPosition(this.cursorPosition);
}
/**
* TODO: @legomushroom
*/
public setCursorPosition(position: Position | null | undefined): boolean {
if (this.cursorPosition === position) {
return false;
}
if (this.cursorPosition && position) {
if (this.cursorPosition.equals(position)) {
return false;
}
}
const wasActive = this.active;
this.cursorPosition = position;
this.didChange = (wasActive !== this.active);
return this.didChange;
}
public override render(
accessor: Pick<IModelDecorationsChangeAccessor, 'changeDecoration' | 'changeDecorationOptions'>,
): this {
if (this.didChange === false) {
return this;
}
super.render(accessor);
this.didChange = false;
return this;
}
}
/**
* Decoration CSS class names.
*/
export enum FrontMatterCssClassNames {
/**
* TODO: @legomushroom
*/
frontMatterHeader = 'prompt-front-matter-header',
frontMatterHeaderInlineInactive = 'prompt-front-matter-header-inline-inactive',
frontMatterHeaderInlineActive = 'prompt-front-matter-header-inline-active',
}
/**
* TODO: @legomushroom
*/
class FrontMatterHeaderDecoration extends ReactiveDecoration<FrontMatterHeader, FrontMatterCssClassNames> {
protected override get isWholeLine(): boolean {
return true;
}
protected override get description(): string {
return 'Front Matter header decoration.';
}
protected override get className(): FrontMatterCssClassNames.frontMatterHeader {
return FrontMatterCssClassNames.frontMatterHeader;
}
protected override get inlineClassName(): FrontMatterCssClassNames.frontMatterHeaderInlineActive | FrontMatterCssClassNames.frontMatterHeaderInlineInactive {
return (this.active)
? FrontMatterCssClassNames.frontMatterHeaderInlineActive
: FrontMatterCssClassNames.frontMatterHeaderInlineInactive;
}
}
/**
* Decoration object.
*/
......@@ -50,13 +250,6 @@ export enum DecorationClassNames {
* CSS class name for `reference` prompt syntax decoration.
*/
reference = 'prompt-reference',
/**
* TODO: @legomushroom
*/
frontMatterHeader = 'prompt-front-matter-header',
frontMatterHeaderInlineInactive = 'prompt-front-matter-header-inline-inactive',
frontMatterHeaderInlineActive = 'prompt-front-matter-header-inline-active',
}
/**
......@@ -74,193 +267,148 @@ export enum DecorationClassNameModifiers {
error = 'squiggly-error', // TODO: @legomushroom - use "markers" instead?
}
/**
* TODO: @legomushroom
*/
type TDecoratedToken = FrontMatterHeader;
/**
* Prompt syntax decorations provider for text models.
*/
export class TextModelPromptDecorator extends ProviderInstanceBase {
/**
* List of IDs of registered text model decorations.
* TODO: @legomushroom
*/
private readonly registeredDecorationIDs: string[] = [];
private readonly decorations: Decoration<BaseToken>[] = [];
constructor(
editor: IPromptFileEditor,
@IPromptsService promptsService: IPromptsService,
) {
super(editor, promptsService);
this.watchCursorPosition();
}
/**
* Handler for the prompt parser update event.
*/
// TODO: @legomushroom - update existing decorations instead of recreating them every time
protected override async onPromptParserUpdate(): Promise<this> {
// TODO: @legomushroom - update existing decorations instead of always recreating them every time
await this.parser.allSettled();
this.removeAllDecorations();
this.addDecorations();
this.addDecorations(this.parser.tokens);
return this;
}
/**
* Add a decorations for all prompt tokens.
* TODO: @legomushroom
*/
private addDecorations(): this {
this.editor.changeDecorations((accessor) => {
private watchCursorPosition(): this {
const interval = setInterval(() => {
const cursorPosition = this.editor.getPosition();
const changedDecorations: Decoration<BaseToken>[] = [];
for (const decoration of this.decorations) {
const decorationID = accessor.addDecoration(
decoration.range,
decoration.options,
);
if ((decoration instanceof ReactiveDecoration) === false) {
continue;
}
this.registeredDecorationIDs.push(decorationID);
if (decoration.setCursorPosition(cursorPosition) === true) {
changedDecorations.push(decoration);
}
}
});
return this;
}
/**
* Get decorations for all currently available prompt tokens.
*/
private get decorations(): readonly ITextModelDecoration[] {
const result: ITextModelDecoration[] = [];
const { tokens } = this.parser;
if (changedDecorations.length === 0) {
return;
}
for (const token of tokens) {
const { range } = token;
this.changeEditorDecorations(changedDecorations);
}, 25);
result.push({
range,
options: this.getDecorationFor(token),
});
}
this._register(toDisposable(() => {
clearInterval(interval);
}));
return result;
return this;
}
/**
* Get decoration options for a provided prompt reference.
* TODO: @legomushroom
*/
private getDecorationFor(token: BaseToken): ModelDecorationOptions {
const isWholeLine = (token instanceof FrontMatterHeaderToken);
return ModelDecorationOptions.createDynamic({
description: this.getDecorationDescription(token),
className: this.getCssClassNameFor(token),
inlineClassName: this.getInlineCssClassNameFor(token),
hoverMessage: this.getHoverMessageFor(token),
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
shouldFillLineOnLineBreak: true,
isWholeLine,
private changeEditorDecorations(
decorations: readonly Decoration<BaseToken>[],
): this {
this.editor.changeDecorations((accessor) => {
for (const decoration of decorations) {
decoration.render(accessor);
}
});
}
/**
* Get decoration description for a provided prompt reference.
*/
private getDecorationDescription(_token: BaseToken): string {
// TODO: @legomushroom
return 'Prompt token decoration.';
return this;
}
/**
* Get CSS class names for a provided prompt reference.
* Add a decorations for all prompt tokens.
*/
private getCssClassNameFor(token: BaseToken): string {
if (token instanceof FrontMatterHeaderToken) {
return DecorationClassNames.frontMatterHeader;
private addDecorations(
tokens: readonly BaseToken[],
): this {
if (tokens.length === 0) {
return this;
}
return DecorationClassNames.default;
// if (token.type === 'file') {
// const mainClassName = DecorationClassNames.reference;
// const { topError } = token;
// if (!topError) {
// return mainClassName;
// }
// const { isRootError } = topError;
// const classNameModifier = (isRootError)
// ? DecorationClassNameModifiers.error
// : DecorationClassNameModifiers.warning;
// return `${mainClassName} ${classNameModifier}`;
// }
// assertNever(
// token.type,
// `Failed to get CSS class name for unknown token type: '${token.type}'.`,
// );
}
private getInlineCssClassNameFor(token: BaseToken): string {
if (token instanceof FrontMatterHeaderToken) {
return DecorationClassNames.frontMatterHeaderInlineInactive;
const decoratedTokens: TDecoratedToken[] = [];
for (const token of tokens) {
if (token instanceof FrontMatterHeader) {
decoratedTokens.push(token);
}
}
return DecorationClassNames.default;
// if (token.type === 'file') {
// const mainClassName = DecorationClassNames.reference;
// const { topError } = token;
// if (!topError) {
// return mainClassName;
// }
// const { isRootError } = topError;
// const classNameModifier = (isRootError)
// ? DecorationClassNameModifiers.error
// : DecorationClassNameModifiers.warning;
// return `${mainClassName} ${classNameModifier}`;
// }
// assertNever(
// token.type,
// `Failed to get CSS class name for unknown token type: '${token.type}'.`,
// );
}
/**
* Get decoration hover message for a provided prompt reference.
*/
private getHoverMessageFor(token: BaseToken): IMarkdownString[] {
if (token instanceof FrontMatterHeaderToken) {
return [new MarkdownString('Front Matter header')];
if (decoratedTokens.length === 0) {
return this;
}
// TODO: @legomushroom
return [];
// if (token.type === 'file') {
// const result = [
// new MarkdownString(basename(token.uri.path)),
// ];
// const { topError } = token;
// if (!topError) {
// return result;
// }
this.editor.changeDecorations((accessor) => {
for (const token of decoratedTokens) {
if (token instanceof FrontMatterHeader) {
const decoration = new FrontMatterHeaderDecoration(
accessor,
token,
);
// const { message, isRootError } = topError;
// const errorCaption = (!isRootError)
// ? localize('warning', "Warning")
// : localize('error', "Error");
this.decorations.push(decoration);
// result.push(new MarkdownString(`[${errorCaption}]: ${message}`));
continue;
}
// return result;
// }
assertNever(
token,
`Unexpected decorated token '${token}'.`,
);
}
});
// assertNever(
// token.type,
// `Failed to create prompt token hover message, unexpected token type: '${token.type}'.`,
// );
return this;
}
/**
* Remove all existing decorations.
*/
private removeAllDecorations(): this {
if (this.decorations.length === 0) {
return this;
}
this.editor.changeDecorations((accessor) => {
for (const decoration of this.registeredDecorationIDs) {
accessor.removeDecoration(decoration);
for (const decoration of this.decorations) {
accessor.removeDecoration(decoration.id);
}
this.decorations.splice(0);
});
this.registeredDecorationIDs.splice(0);
return this;
}
......@@ -272,11 +420,9 @@ export class TextModelPromptDecorator extends ProviderInstanceBase {
return `text-model-prompt-decorator:${this.model.uri.path}`;
}
/**
* @inheritdoc
*/
public override dispose(): void {
this.removeAllDecorations();
super.dispose();
}
}
......@@ -327,7 +473,7 @@ const registerFrontMatterStyles = (
`background-color: ${theme.getColor(frontMatterHeaderBackgroundColor)};`,
);
const frontMatterHeaderCssSelector = `.monaco-editor .${DecorationClassNames.frontMatterHeader}`;
const frontMatterHeaderCssSelector = `.monaco-editor .${FrontMatterCssClassNames.frontMatterHeader}`;
collector.addRule(
`${frontMatterHeaderCssSelector} { ${styles.join(' ')} }`,
);
......@@ -338,12 +484,12 @@ const registerFrontMatterStyles = (
const inlineActiveStyles = [];
inlineActiveStyles.push('color: var(--vscode-foreground);');
const frontMatterHeaderInlineActiveCssSelector = `.monaco-editor .${DecorationClassNames.frontMatterHeaderInlineActive}`;
const frontMatterHeaderInlineActiveCssSelector = `.monaco-editor .${FrontMatterCssClassNames.frontMatterHeaderInlineActive}`;
collector.addRule(
`${frontMatterHeaderInlineActiveCssSelector} { ${inlineActiveStyles.join(' ')} }`,
);
const frontMatterHeaderInlineInactiveCssSelector = `.monaco-editor .${DecorationClassNames.frontMatterHeaderInlineInactive}`;
const frontMatterHeaderInlineInactiveCssSelector = `.monaco-editor .${FrontMatterCssClassNames.frontMatterHeaderInlineInactive}`;
collector.addRule(
`${frontMatterHeaderInlineInactiveCssSelector} { ${inlineInactiveStyles.join(' ')} }`,
);
......@@ -353,5 +499,7 @@ const registerFrontMatterStyles = (
* Provider for prompt syntax decorators on text models.
*/
export class PromptDecoratorsInstanceManager extends ProviderInstanceManagerBase<TextModelPromptDecorator> {
protected override readonly InstanceClass = TextModelPromptDecorator;
protected override get InstanceClass() {
return TextModelPromptDecorator;
}
}
......@@ -19,7 +19,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../ba
import { CarriageReturn } from '../../../../../../../editor/common/codecs/linesCodec/tokens/carriageReturn.js';
import { Colon, Dash, Space, Tab, VerticalTab } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/index.js';
import { MarkdownExtensionsDecoder } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/markdownExtensionsDecoder.js';
import { FrontMatterHeaderToken } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeaderToken.js';
import { FrontMatterHeader } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js';
import { FrontMatterMarker, TMarkerToken } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.js';
/**
......@@ -175,7 +175,7 @@ suite('MarkdownExtensionsDecoder', () => {
promptContents.join(newLine),
[
// header
new FrontMatterHeaderToken(
new FrontMatterHeader(
new Range(1, 1, 4, 1 + markerLength + newLine.length),
startMarker,
Text.fromTokens([
......@@ -241,7 +241,7 @@ suite('MarkdownExtensionsDecoder', () => {
promptContents.join(newLine),
[
// header
new FrontMatterHeaderToken(
new FrontMatterHeader(
new Range(1, 1, 5, 1 + markerLength + newLine.length),
startMarker,
Text.fromTokens([
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать