diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/decorationBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/decorationBase.ts new file mode 100644 index 00000000000..4480a6de77a --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/decorationBase.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../../../../../../../editor/common/core/range.js'; +import { IMarkdownString } from '../../../../../../../../../base/common/htmlContent.js'; +import { BaseToken } from '../../../../../../../../../editor/common/codecs/baseToken.js'; +import { ModelDecorationOptions } from '../../../../../../../../../editor/common/model/textModel.js'; +import { IModelDecorationsChangeAccessor, TrackedRangeStickiness } from '../../../../../../../../../editor/common/model.js'; +import { IColorTheme, ICssStyleCollector } from '../../../../../../../../../platform/theme/common/themeService.js'; + +/** + * Base class for all editor decorations. + */ +export abstract class DecorationBase< + TPromptToken extends BaseToken, + TCssClassName extends string = string, +> { + /** + * Description of the decoration. + */ + protected abstract get description(): string; + + /** + * Default CSS class name of the decoration. + */ + protected abstract get className(): TCssClassName; + + /** + * Inline CSS class name of the decoration. + */ + protected abstract get inlineClassName(): TCssClassName; + + /** + * Indicates whether the decoration spans the whole line(s). + */ + protected get isWholeLine(): boolean { + return false; + } + + /** + * Hover message of the decoration. + */ + protected get hoverMessage(): IMarkdownString | IMarkdownString[] | null { + return null; + } + + /** + * ID of editor decoration it was registered with. + */ + public readonly id: string; + + constructor( + accessor: Pick, + protected readonly token: TPromptToken, + ) { + this.id = accessor.addDecoration(this.range, this.decorationOptions); + } + + /** + * Range of the decoration. + */ + public get range(): Range { + return this.token.range; + } + + /** + * Renders (updates) the decoration in the editor. + */ + public render( + accessor: Pick, + ): this { + accessor.changeDecorationOptions( + this.id, + this.decorationOptions, + ); + + return this; + } + + /** + * Removes associated editor decoration(s). + */ + public remove( + accessor: Pick, + ): this { + accessor.removeDecoration(this.id); + + return this; + } + + /** + * Get editor decoration options for this decorator. + */ + private 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, + }); + } +} + +/** + * Type of a generic decoration class. + */ +export type TDecorationClass = { + new( + accessor: Pick, + token: TPromptToken, + ): DecorationBase; + + /** + * Register CSS styles for the decoration. + */ + registerStyles( + theme: IColorTheme, + collector: ICssStyleCollector, + ): void; + + /** + * Whether the decoration class handles the provided token. + */ + handles(token: BaseToken): token is TPromptToken; +}; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/frontMatterDecoration.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/frontMatterDecoration.ts new file mode 100644 index 00000000000..f835150bb02 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/frontMatterDecoration.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../../../../../nls.js'; +import { ReactiveDecorationBase } from './reactiveDecorationBase.js'; +import { Color, RGBA } from '../../../../../../../../../base/common/color.js'; +import { BaseToken } from '../../../../../../../../../editor/common/codecs/baseToken.js'; +import { registerColor } from '../../../../../../../../../platform/theme/common/colorUtils.js'; +import { contrastBorder } from '../../../../../../../../../platform/theme/common/colorRegistry.js'; +import { IColorTheme, ICssStyleCollector } from '../../../../../../../../../platform/theme/common/themeService.js'; +import { FrontMatterHeader } from '../../../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; + +/** + * 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 + */ +export class FrontMatterHeaderDecoration extends ReactiveDecorationBase { + protected override get isWholeLine(): boolean { + return true; + } + + protected override get description(): string { + return 'Front Matter header editor 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; + } + + /** + * Whether current decoration class can decorate provided token. + */ + public static handles( + token: BaseToken, + ): token is FrontMatterHeader { + return token instanceof FrontMatterHeader; + } + + /** + * Register CSS styles of the decoration. + */ + public static registerStyles( + theme: IColorTheme, + collector: ICssStyleCollector, + ) { + /** + * TODO: @legomushroom + */ + const frontMatterHeaderBackgroundColor = registerColor( + 'chat.prompt.frontMatterBackground', + { dark: new Color(new RGBA(0, 0, 0, 0.20)), light: new Color(new RGBA(0, 0, 0, 0.10)), hcDark: contrastBorder, hcLight: contrastBorder, }, + localize('chat.prompt.frontMatterBackground', "background color of a Front Matter header block."), + ); + + const styles = []; + styles.push( + `background-color: ${theme.getColor(frontMatterHeaderBackgroundColor)};`, + ); + + const frontMatterHeaderCssSelector = `.monaco-editor .${FrontMatterCssClassNames.frontMatterHeader}`; + collector.addRule( + `${frontMatterHeaderCssSelector} { ${styles.join(' ')} }`, + ); + + const inlineInactiveStyles = []; + inlineInactiveStyles.push('color: var(--vscode-disabledForeground);'); + + const inlineActiveStyles = []; + inlineActiveStyles.push('color: var(--vscode-foreground);'); + + const frontMatterHeaderInlineActiveCssSelector = `.monaco-editor .${FrontMatterCssClassNames.frontMatterHeaderInlineActive}`; + collector.addRule( + `${frontMatterHeaderInlineActiveCssSelector} { ${inlineActiveStyles.join(' ')} }`, + ); + + const frontMatterHeaderInlineInactiveCssSelector = `.monaco-editor .${FrontMatterCssClassNames.frontMatterHeaderInlineInactive}`; + collector.addRule( + `${frontMatterHeaderInlineInactiveCssSelector} { ${inlineInactiveStyles.join(' ')} }`, + ); + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/reactiveDecorationBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/reactiveDecorationBase.ts new file mode 100644 index 00000000000..f02c91fa9c5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/reactiveDecorationBase.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DecorationBase } from './decorationBase.js'; +import { Position } from '../../../../../../../../../editor/common/core/position.js'; +import { BaseToken } from '../../../../../../../../../editor/common/codecs/baseToken.js'; +import { IModelDecorationsChangeAccessor } from '../../../../../../../../../editor/common/model.js'; + +/** + * Base class for all reactive editor decorations. A reactive decoration + * is a decoration that can change its appearance based on current cursor + * position in the editor, hence can "react" to the user's actions. + */ +export abstract class ReactiveDecorationBase< + TPromptToken extends BaseToken, + TCssClassName extends string = string, +> extends DecorationBase { + /** + * Whether the decoration has changed since the last {@link render}. + */ + public get changed(): boolean { + return this.didChange; + } + + /** + * Current position of cursor in the editor. + */ + private cursorPosition?: Position | null; + + /** + * Private field for the {@link changed} property. + */ + private didChange = true; + + constructor( + accessor: Pick, + 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); + } + + /** + * Set cursor position and update {@link changed} property if needed. + */ + public setCursorPosition(position: Position | null | undefined): this is { readonly changed: true } { + 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, + ): this { + if (this.didChange === false) { + return this; + } + + super.render(accessor); + this.didChange = false; + + return this; + } +} + +/** + * Type for a decorator with {@link ReactiveDecorationBase.changed changed} property set to `true`. + */ +export type TChangedDecorator = ReactiveDecorationBase & { readonly changed: true }; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts new file mode 100644 index 00000000000..fa8dc69e778 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts @@ -0,0 +1,181 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptsService } from '../../../service/types.js'; +import { ProviderInstanceBase } from '../providerInstanceBase.js'; +import { toDisposable } from '../../../../../../../../base/common/lifecycle.js'; +import { DecorationBase, TDecorationClass } from './decorations/decorationBase.js'; +import { FrontMatterHeaderDecoration } from './decorations/frontMatterDecoration.js'; +import { BaseToken } from '../../../../../../../../editor/common/codecs/baseToken.js'; +import { IPromptFileEditor, ProviderInstanceManagerBase } from '../providerInstanceManagerBase.js'; +import { ReactiveDecorationBase, TChangedDecorator } from './decorations/reactiveDecorationBase.js'; +import { registerThemingParticipant } from '../../../../../../../../platform/theme/common/themeService.js'; +import { FrontMatterHeader } from '../../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; + +/** + * Prompt tokens that are decorated by this provider. + */ +type TDecoratedToken = FrontMatterHeader; + +/** + * List of all supported decorations. + */ +const SUPPORTED_DECORATIONS: readonly TDecorationClass[] = Object.freeze([ + FrontMatterHeaderDecoration, +]); + +/** + * Prompt syntax decorations provider for text models. + */ +export class TextModelPromptDecorator extends ProviderInstanceBase { + /** + * Currently active decorations. + */ + private readonly decorations: DecorationBase[] = []; + + constructor( + editor: IPromptFileEditor, + @IPromptsService promptsService: IPromptsService, + ) { + super(editor, promptsService); + + this.watchCursorPosition(); + } + + // TODO: @legomushroom - update existing decorations instead of recreating them every time + protected override async onPromptParserUpdate(): Promise { + await this.parser.allSettled(); + + this.removeAllDecorations(); + this.addDecorations(this.parser.tokens); + + return this; + } + + /** + * Watch editor cursor position and update reactive decorations accordingly. + */ + private watchCursorPosition(): this { + const interval = setInterval(() => { + const cursorPosition = this.editor.getPosition(); + + const changedDecorations: TChangedDecorator[] = []; + for (const decoration of this.decorations) { + if ((decoration instanceof ReactiveDecorationBase) === false) { + continue; + } + + if (decoration.setCursorPosition(cursorPosition) === true) { + changedDecorations.push(decoration); + } + } + + if (changedDecorations.length === 0) { + return; + } + + this.changeEditorDecorations(changedDecorations); + }, 25); + + this._register(toDisposable(() => { + clearInterval(interval); + })); + + return this; + } + + /** + * + */ + private changeEditorDecorations( + decorations: readonly TChangedDecorator[], + ): this { + this.editor.changeDecorations((accessor) => { + for (const decoration of decorations) { + decoration.render(accessor); + } + }); + + return this; + } + + /** + * Add a decorations for all prompt tokens. + */ + private addDecorations( + tokens: readonly BaseToken[], + ): this { + if (tokens.length === 0) { + return this; + } + + this.editor.changeDecorations((accessor) => { + for (const token of tokens) { + for (const Decoration of SUPPORTED_DECORATIONS) { + if (Decoration.handles(token) === false) { + continue; + } + + this.decorations.push( + new Decoration(accessor, token), + ); + break; + } + } + }); + + 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.decorations) { + decoration.remove(accessor); + } + + this.decorations.splice(0); + }); + + return this; + } + + public override dispose(): void { + this.removeAllDecorations(); + + super.dispose(); + } + + /** + * Returns a string representation of this object. + */ + public override toString() { + return `text-model-prompt-decorator:${this.model.uri.path}`; + } +} + +/** + * Register CSS styles of the supported decorations. + */ +registerThemingParticipant((theme, collector) => { + for (const Decoration of SUPPORTED_DECORATIONS) { + Decoration.registerStyles(theme, collector); + } +}); + +/** + * Provider for prompt syntax decorators on text models. + */ +export class PromptDecorationsProviderInstanceManager extends ProviderInstanceManagerBase { + protected override get InstanceClass() { + return TextModelPromptDecorator; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/index.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/index.ts index 0dce23b9e0b..e47e413738d 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/index.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/index.ts @@ -6,10 +6,10 @@ import { PromptLinkProvider } from './promptLinkProvider.js'; import { isWindows } from '../../../../../../../base/common/platform.js'; import { PromptPathAutocompletion } from './promptPathAutocompletion.js'; -import { PromptDecoratorsInstanceManager } from './textModelPromptDecorator.js'; import { Registry } from '../../../../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../../../../services/lifecycle/common/lifecycle.js'; import { PromptLinkDiagnosticsInstanceManager } from './promptLinkDiagnosticsProvider.js'; +import { PromptDecorationsProviderInstanceManager } from './decorationsProvider/promptDecorationsProvider.js'; import { BrandedService } from '../../../../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContributionsRegistry, Extensions, IWorkbenchContribution } from '../../../../../../common/contributions.js'; @@ -18,8 +18,8 @@ import { IWorkbenchContributionsRegistry, Extensions, IWorkbenchContribution } f */ export const registerReusablePromptLanguageFeatures = () => { registerContribution(PromptLinkProvider); - registerContribution(PromptDecoratorsInstanceManager); registerContribution(PromptLinkDiagnosticsInstanceManager); + registerContribution(PromptDecorationsProviderInstanceManager); /** * We restrict this provider to `Unix` machines for now because of @@ -42,5 +42,4 @@ const registerContribution = ( ) => { Registry.as(Extensions.Workbench) .registerWorkbenchContribution(contribution, LifecyclePhase.Eventually); - }; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/textModelPromptDecorator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/textModelPromptDecorator.ts deleted file mode 100644 index 6c951c6aeb9..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/textModelPromptDecorator.ts +++ /dev/null @@ -1,505 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -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 { 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 { ModelDecorationOptions } from '../../../../../../../editor/common/model/textModel.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'; - -/** - * TODO: @legomushroom - list - * - add active/inactive logic for front matter header - */ - -/** - * TODO: @legomushroom - */ -abstract class Decoration { - /** - * 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, - 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, - ): 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 extends Decoration { - /** - * 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, - 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, - ): 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 { - 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. - */ -export interface ITextModelDecoration { - /** - * Range of the decoration. - */ - range: IRange; - - /** - * Associated decoration options. - */ - options: ModelDecorationOptions; -} - -/** - * Decoration CSS class names. - */ -export enum DecorationClassNames { - /** - * CSS class name for `default` prompt syntax decoration. - */ - default = 'prompt-decoration', - - /** - * CSS class name for `reference` prompt syntax decoration. - */ - reference = 'prompt-reference', -} - -/** - * Decoration CSS class name modifiers. - */ -export enum DecorationClassNameModifiers { - /** - * CSS class name for `warning` modifier. - */ - warning = 'squiggly-warning', - - /** - * CSS class name for `error` modifier. - */ - 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 { - /** - * TODO: @legomushroom - */ - private readonly decorations: Decoration[] = []; - - 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 { - await this.parser.allSettled(); - - this.removeAllDecorations(); - this.addDecorations(this.parser.tokens); - - return this; - } - - /** - * TODO: @legomushroom - */ - private watchCursorPosition(): this { - const interval = setInterval(() => { - const cursorPosition = this.editor.getPosition(); - - const changedDecorations: Decoration[] = []; - for (const decoration of this.decorations) { - if ((decoration instanceof ReactiveDecoration) === false) { - continue; - } - - if (decoration.setCursorPosition(cursorPosition) === true) { - changedDecorations.push(decoration); - } - } - - if (changedDecorations.length === 0) { - return; - } - - this.changeEditorDecorations(changedDecorations); - }, 25); - - this._register(toDisposable(() => { - clearInterval(interval); - })); - - return this; - } - - /** - * TODO: @legomushroom - */ - private changeEditorDecorations( - decorations: readonly Decoration[], - ): this { - this.editor.changeDecorations((accessor) => { - for (const decoration of decorations) { - decoration.render(accessor); - } - }); - - return this; - } - - /** - * Add a decorations for all prompt tokens. - */ - private addDecorations( - tokens: readonly BaseToken[], - ): this { - if (tokens.length === 0) { - return this; - } - - const decoratedTokens: TDecoratedToken[] = []; - for (const token of tokens) { - if (token instanceof FrontMatterHeader) { - decoratedTokens.push(token); - } - } - - if (decoratedTokens.length === 0) { - return this; - } - - this.editor.changeDecorations((accessor) => { - for (const token of decoratedTokens) { - if (token instanceof FrontMatterHeader) { - const decoration = new FrontMatterHeaderDecoration( - accessor, - token, - ); - - this.decorations.push(decoration); - - continue; - } - - assertNever( - token, - `Unexpected decorated token '${token}'.`, - ); - } - }); - - 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.decorations) { - accessor.removeDecoration(decoration.id); - } - - this.decorations.splice(0); - }); - - return this; - } - - /** - * Returns a string representation of this object. - */ - public override toString() { - return `text-model-prompt-decorator:${this.model.uri.path}`; - } - - public override dispose(): void { - this.removeAllDecorations(); - - super.dispose(); - } -} - -/** - * Register CSS styles. - */ -registerThemingParticipant((theme, collector) => { - const styles = ['border-radius: 3px;']; - - const backgroundColor = theme.getColor(chatSlashCommandBackground); - if (backgroundColor) { - styles.push(`background-color: ${backgroundColor};`); - } - - // TODO: @legomushroom - // const color = theme.getColor(chatSlashCommandForeground); - // if (color) { - // styles.push(`color: ${color};`); - // } - - const defaultCssSelector = `.monaco-editor .${DecorationClassNames.default}`; - collector.addRule( - `${defaultCssSelector} { ${styles.join(' ')} }`, - ); - - registerFrontMatterStyles(theme, collector); -}); - -/** - * TODO: @legomushroom - */ -const frontMatterHeaderBackgroundColor = registerColor( - 'chat.prompt.frontMatterBackground', - { dark: new Color(new RGBA(0, 0, 0, 0.20)), light: new Color(new RGBA(0, 0, 0, 0.10)), hcDark: contrastBorder, hcLight: contrastBorder, }, - localize('chat.prompt.frontMatterBackground', "background color of a Front Matter header block."), -); - -/** - * TODO: @legomushroom - */ -const registerFrontMatterStyles = ( - theme: IColorTheme, - collector: ICssStyleCollector, -) => { - const styles = []; - styles.push( - `background-color: ${theme.getColor(frontMatterHeaderBackgroundColor)};`, - ); - - const frontMatterHeaderCssSelector = `.monaco-editor .${FrontMatterCssClassNames.frontMatterHeader}`; - collector.addRule( - `${frontMatterHeaderCssSelector} { ${styles.join(' ')} }`, - ); - - const inlineInactiveStyles = []; - inlineInactiveStyles.push('color: var(--vscode-disabledForeground);'); - - const inlineActiveStyles = []; - inlineActiveStyles.push('color: var(--vscode-foreground);'); - - const frontMatterHeaderInlineActiveCssSelector = `.monaco-editor .${FrontMatterCssClassNames.frontMatterHeaderInlineActive}`; - collector.addRule( - `${frontMatterHeaderInlineActiveCssSelector} { ${inlineActiveStyles.join(' ')} }`, - ); - - const frontMatterHeaderInlineInactiveCssSelector = `.monaco-editor .${FrontMatterCssClassNames.frontMatterHeaderInlineInactive}`; - collector.addRule( - `${frontMatterHeaderInlineInactiveCssSelector} { ${inlineInactiveStyles.join(' ')} }`, - ); -}; - -/** - * Provider for prompt syntax decorators on text models. - */ -export class PromptDecoratorsInstanceManager extends ProviderInstanceManagerBase { - protected override get InstanceClass() { - return TextModelPromptDecorator; - } -}