Path: blob/main/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './codeBlockPart.css';67import * as dom from '../../../../base/browser/dom.js';8import { renderFormattedText } from '../../../../base/browser/formattedTextRenderer.js';9import { Button } from '../../../../base/browser/ui/button/button.js';10import { CancellationToken } from '../../../../base/common/cancellation.js';11import { Codicon } from '../../../../base/common/codicons.js';12import { Emitter, Event } from '../../../../base/common/event.js';13import { combinedDisposable, Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';14import { Schemas } from '../../../../base/common/network.js';15import { isEqual } from '../../../../base/common/resources.js';16import { assertType } from '../../../../base/common/types.js';17import { URI, UriComponents } from '../../../../base/common/uri.js';18import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js';19import { IDiffEditor } from '../../../../editor/browser/editorBrowser.js';20import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js';21import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';22import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';23import { DiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/diffEditorWidget.js';24import { EDITOR_FONT_DEFAULTS, EditorOption, IEditorOptions } from '../../../../editor/common/config/editorOptions.js';25import { IRange, Range } from '../../../../editor/common/core/range.js';26import { ScrollType } from '../../../../editor/common/editorCommon.js';27import { TextEdit } from '../../../../editor/common/languages.js';28import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js';29import { TextModelText } from '../../../../editor/common/model/textModelText.js';30import { IModelService } from '../../../../editor/common/services/model.js';31import { DefaultModelSHA1Computer } from '../../../../editor/common/services/modelService.js';32import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js';33import { BracketMatchingController } from '../../../../editor/contrib/bracketMatching/browser/bracketMatching.js';34import { ColorDetector } from '../../../../editor/contrib/colorPicker/browser/colorDetector.js';35import { ContextMenuController } from '../../../../editor/contrib/contextmenu/browser/contextmenu.js';36import { GotoDefinitionAtPositionEditorContribution } from '../../../../editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.js';37import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js';38import { GlyphHoverController } from '../../../../editor/contrib/hover/browser/glyphHoverController.js';39import { LinkDetector } from '../../../../editor/contrib/links/browser/links.js';40import { MessageController } from '../../../../editor/contrib/message/browser/messageController.js';41import { ViewportSemanticTokensContribution } from '../../../../editor/contrib/semanticTokens/browser/viewportSemanticTokens.js';42import { SmartSelectController } from '../../../../editor/contrib/smartSelect/browser/smartSelect.js';43import { WordHighlighterContribution } from '../../../../editor/contrib/wordHighlighter/browser/wordHighlighter.js';44import { localize } from '../../../../nls.js';45import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';46import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';47import { MenuId } from '../../../../platform/actions/common/actions.js';48import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';49import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';50import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';51import { FileKind } from '../../../../platform/files/common/files.js';52import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';53import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';54import { ILabelService } from '../../../../platform/label/common/label.js';55import { IOpenerService } from '../../../../platform/opener/common/opener.js';56import { ResourceLabel } from '../../../browser/labels.js';57import { ResourceContextKey } from '../../../common/contextkeys.js';58import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';59import { InspectEditorTokensController } from '../../codeEditor/browser/inspectEditorTokens/inspectEditorTokens.js';60import { MenuPreventer } from '../../codeEditor/browser/menuPreventer.js';61import { SelectionClipboardContributionID } from '../../codeEditor/browser/selectionClipboard.js';62import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js';63import { IMarkdownVulnerability } from '../common/annotations.js';64import { ChatContextKeys } from '../common/chatContextKeys.js';65import { IChatResponseModel, IChatTextEditGroup } from '../common/chatModel.js';66import { IChatResponseViewModel, isRequestVM, isResponseVM } from '../common/chatViewModel.js';67import { ChatTreeItem } from './chat.js';68import { IChatRendererDelegate } from './chatListRenderer.js';69import { ChatEditorOptions } from './chatOptions.js';70import { emptyProgressRunner, IEditorProgressService } from '../../../../platform/progress/common/progress.js';71import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js';72import { SnippetController2 } from '../../../../editor/contrib/snippet/browser/snippetController2.js';7374const $ = dom.$;7576export interface ICodeBlockData {77readonly codeBlockIndex: number;78readonly codeBlockPartIndex: number;79readonly element: unknown;8081readonly textModel: Promise<ITextModel>;82readonly languageId: string;8384readonly codemapperUri?: URI;8586readonly vulns?: readonly IMarkdownVulnerability[];87readonly range?: Range;8889readonly parentContextKeyService?: IContextKeyService;90readonly renderOptions?: ICodeBlockRenderOptions;9192readonly chatSessionId: string;93}9495/**96* Special markdown code block language id used to render a local file.97*98* The text of the code path should be a {@link LocalFileCodeBlockData} json object.99*/100export const localFileLanguageId = 'vscode-local-file';101102103export function parseLocalFileData(text: string) {104105interface RawLocalFileCodeBlockData {106readonly uri: UriComponents;107readonly range?: IRange;108}109110let data: RawLocalFileCodeBlockData;111try {112data = JSON.parse(text);113} catch (e) {114throw new Error('Could not parse code block local file data');115}116117let uri: URI;118try {119uri = URI.revive(data?.uri);120} catch (e) {121throw new Error('Invalid code block local file data URI');122}123124let range: IRange | undefined;125if (data.range) {126// Note that since this is coming from extensions, position are actually zero based and must be converted.127range = new Range(data.range.startLineNumber + 1, data.range.startColumn + 1, data.range.endLineNumber + 1, data.range.endColumn + 1);128}129130return { uri, range };131}132133export interface ICodeBlockActionContext {134readonly code: string;135readonly codemapperUri?: URI;136readonly languageId?: string;137readonly codeBlockIndex: number;138readonly element: unknown;139140readonly chatSessionId: string | undefined;141}142143export interface ICodeBlockRenderOptions {144hideToolbar?: boolean;145verticalPadding?: number;146reserveWidth?: number;147editorOptions?: IEditorOptions;148maxHeightInLines?: number;149}150151const defaultCodeblockPadding = 10;152export class CodeBlockPart extends Disposable {153protected readonly _onDidChangeContentHeight = this._register(new Emitter<void>());154public readonly onDidChangeContentHeight = this._onDidChangeContentHeight.event;155156public readonly editor: CodeEditorWidget;157protected readonly toolbar: MenuWorkbenchToolBar;158private readonly contextKeyService: IContextKeyService;159160public readonly element: HTMLElement;161162private readonly vulnsButton: Button;163private readonly vulnsListElement: HTMLElement;164165private currentCodeBlockData: ICodeBlockData | undefined;166private currentScrollWidth = 0;167168private isDisposed = false;169170private resourceContextKey: ResourceContextKey;171172private get verticalPadding(): number {173return this.currentCodeBlockData?.renderOptions?.verticalPadding ?? defaultCodeblockPadding;174}175176constructor(177private readonly editorOptions: ChatEditorOptions,178readonly menuId: MenuId,179delegate: IChatRendererDelegate,180overflowWidgetsDomNode: HTMLElement | undefined,181private readonly isSimpleWidget: boolean = false,182@IInstantiationService instantiationService: IInstantiationService,183@IContextKeyService contextKeyService: IContextKeyService,184@IModelService protected readonly modelService: IModelService,185@IConfigurationService private readonly configurationService: IConfigurationService,186@IAccessibilityService private readonly accessibilityService: IAccessibilityService,187) {188super();189this.element = $('.interactive-result-code-block');190191this.resourceContextKey = this._register(instantiationService.createInstance(ResourceContextKey));192this.contextKeyService = this._register(contextKeyService.createScoped(this.element));193const scopedInstantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])));194const editorElement = dom.append(this.element, $('.interactive-result-editor'));195this.editor = this.createEditor(scopedInstantiationService, editorElement, {196...getSimpleEditorOptions(this.configurationService),197readOnly: true,198lineNumbers: 'off',199selectOnLineNumbers: true,200scrollBeyondLastLine: false,201lineDecorationsWidth: 8,202dragAndDrop: false,203padding: { top: this.verticalPadding, bottom: this.verticalPadding },204mouseWheelZoom: false,205scrollbar: {206vertical: 'hidden',207alwaysConsumeMouseWheel: false208},209definitionLinkOpensInPeek: false,210gotoLocation: {211multiple: 'goto',212multipleDeclarations: 'goto',213multipleDefinitions: 'goto',214multipleImplementations: 'goto',215},216ariaLabel: localize('chat.codeBlockHelp', 'Code block'),217overflowWidgetsDomNode,218tabFocusMode: true,219...this.getEditorOptionsFromConfig(),220});221222const toolbarElement = dom.append(this.element, $('.interactive-result-code-block-toolbar'));223const editorScopedService = this.editor.contextKeyService.createScoped(toolbarElement);224const editorScopedInstantiationService = this._register(scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService])));225this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarElement, menuId, {226menuOptions: {227shouldForwardArgs: true228}229}));230231const vulnsContainer = dom.append(this.element, $('.interactive-result-vulns'));232const vulnsHeaderElement = dom.append(vulnsContainer, $('.interactive-result-vulns-header', undefined));233this.vulnsButton = this._register(new Button(vulnsHeaderElement, {234buttonBackground: undefined,235buttonBorder: undefined,236buttonForeground: undefined,237buttonHoverBackground: undefined,238buttonSecondaryBackground: undefined,239buttonSecondaryForeground: undefined,240buttonSecondaryHoverBackground: undefined,241buttonSeparator: undefined,242supportIcons: true243}));244245this.vulnsListElement = dom.append(vulnsContainer, $('ul.interactive-result-vulns-list'));246247this._register(this.vulnsButton.onDidClick(() => {248const element = this.currentCodeBlockData!.element as IChatResponseViewModel;249element.vulnerabilitiesListExpanded = !element.vulnerabilitiesListExpanded;250this.vulnsButton.label = this.getVulnerabilitiesLabel();251this.element.classList.toggle('chat-vulnerabilities-collapsed', !element.vulnerabilitiesListExpanded);252this._onDidChangeContentHeight.fire();253// this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded);254}));255256this._register(this.toolbar.onDidChangeDropdownVisibility(e => {257toolbarElement.classList.toggle('force-visibility', e);258}));259260this._configureForScreenReader();261this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this._configureForScreenReader()));262this._register(this.configurationService.onDidChangeConfiguration((e) => {263if (e.affectedKeys.has(AccessibilityVerbositySettingId.Chat)) {264this._configureForScreenReader();265}266}));267268this._register(this.editorOptions.onDidChange(() => {269this.editor.updateOptions(this.getEditorOptionsFromConfig());270}));271272this._register(this.editor.onDidScrollChange(e => {273this.currentScrollWidth = e.scrollWidth;274}));275this._register(this.editor.onDidContentSizeChange(e => {276if (e.contentHeightChanged) {277this._onDidChangeContentHeight.fire();278}279}));280this._register(this.editor.onDidBlurEditorWidget(() => {281this.element.classList.remove('focused');282WordHighlighterContribution.get(this.editor)?.stopHighlighting();283this.clearWidgets();284}));285this._register(this.editor.onDidFocusEditorWidget(() => {286this.element.classList.add('focused');287WordHighlighterContribution.get(this.editor)?.restoreViewState(true);288}));289this._register(Event.any(290this.editor.onDidChangeModel,291this.editor.onDidChangeModelContent292)(() => {293if (this.currentCodeBlockData) {294this.updateContexts(this.currentCodeBlockData);295}296}));297298// Parent list scrolled299if (delegate.onDidScroll) {300this._register(delegate.onDidScroll(e => {301this.clearWidgets();302}));303}304}305306override dispose() {307this.isDisposed = true;308super.dispose();309}310311get uri(): URI | undefined {312return this.editor.getModel()?.uri;313}314315private createEditor(instantiationService: IInstantiationService, parent: HTMLElement, options: Readonly<IEditorConstructionOptions>): CodeEditorWidget {316return this._register(instantiationService.createInstance(CodeEditorWidget, parent, options, {317isSimpleWidget: this.isSimpleWidget,318contributions: EditorExtensionsRegistry.getSomeEditorContributions([319MenuPreventer.ID,320SelectionClipboardContributionID,321ContextMenuController.ID,322323WordHighlighterContribution.ID,324ViewportSemanticTokensContribution.ID,325BracketMatchingController.ID,326SmartSelectController.ID,327ContentHoverController.ID,328GlyphHoverController.ID,329MessageController.ID,330GotoDefinitionAtPositionEditorContribution.ID,331SuggestController.ID,332SnippetController2.ID,333ColorDetector.ID,334LinkDetector.ID,335336InspectEditorTokensController.ID,337])338}));339}340341focus(): void {342this.editor.focus();343}344345private updatePaddingForLayout() {346// scrollWidth = "the width of the content that needs to be scrolled"347// contentWidth = "the width of the area where content is displayed"348const horizontalScrollbarVisible = this.currentScrollWidth > this.editor.getLayoutInfo().contentWidth;349const scrollbarHeight = this.editor.getLayoutInfo().horizontalScrollbarHeight;350const bottomPadding = horizontalScrollbarVisible ?351Math.max(this.verticalPadding - scrollbarHeight, 2) :352this.verticalPadding;353this.editor.updateOptions({ padding: { top: this.verticalPadding, bottom: bottomPadding } });354}355356private _configureForScreenReader(): void {357const toolbarElt = this.toolbar.getElement();358if (this.accessibilityService.isScreenReaderOptimized()) {359toolbarElt.style.display = 'block';360} else {361toolbarElt.style.display = '';362}363}364365private getEditorOptionsFromConfig(): IEditorOptions {366return {367wordWrap: this.editorOptions.configuration.resultEditor.wordWrap,368fontLigatures: this.editorOptions.configuration.resultEditor.fontLigatures,369bracketPairColorization: this.editorOptions.configuration.resultEditor.bracketPairColorization,370fontFamily: this.editorOptions.configuration.resultEditor.fontFamily === 'default' ?371EDITOR_FONT_DEFAULTS.fontFamily :372this.editorOptions.configuration.resultEditor.fontFamily,373fontSize: this.editorOptions.configuration.resultEditor.fontSize,374fontWeight: this.editorOptions.configuration.resultEditor.fontWeight,375lineHeight: this.editorOptions.configuration.resultEditor.lineHeight,376...this.currentCodeBlockData?.renderOptions?.editorOptions,377};378}379380layout(width: number): void {381const contentHeight = this.getContentHeight();382383let height = contentHeight;384if (this.currentCodeBlockData?.renderOptions?.maxHeightInLines) {385height = Math.min(contentHeight, this.editor.getOption(EditorOption.lineHeight) * this.currentCodeBlockData?.renderOptions?.maxHeightInLines);386}387388const editorBorder = 2;389width = width - editorBorder - (this.currentCodeBlockData?.renderOptions?.reserveWidth ?? 0);390this.editor.layout({ width: isRequestVM(this.currentCodeBlockData?.element) ? width * 0.9 : width, height });391this.updatePaddingForLayout();392}393394private getContentHeight() {395if (this.currentCodeBlockData?.range) {396const lineCount = this.currentCodeBlockData.range.endLineNumber - this.currentCodeBlockData.range.startLineNumber + 1;397const lineHeight = this.editor.getOption(EditorOption.lineHeight);398return lineCount * lineHeight;399}400return this.editor.getContentHeight();401}402403async render(data: ICodeBlockData, width: number) {404this.currentCodeBlockData = data;405if (data.parentContextKeyService) {406this.contextKeyService.updateParent(data.parentContextKeyService);407}408409if (this.getEditorOptionsFromConfig().wordWrap === 'on') {410// Initialize the editor with the new proper width so that getContentHeight411// will be computed correctly in the next call to layout()412this.layout(width);413}414415const didUpdate = await this.updateEditor(data);416if (!didUpdate || this.isDisposed || this.currentCodeBlockData !== data) {417return;418}419420this.editor.updateOptions({421...this.getEditorOptionsFromConfig(),422});423if (!this.editor.getOption(EditorOption.ariaLabel)) {424// Don't override the ariaLabel if it was set by the editor options425this.editor.updateOptions({426ariaLabel: localize('chat.codeBlockLabel', "Code block {0}", data.codeBlockIndex + 1),427});428}429this.layout(width);430this.toolbar.setAriaLabel(localize('chat.codeBlockToolbarLabel', "Code block {0}", data.codeBlockIndex + 1));431if (data.renderOptions?.hideToolbar) {432dom.hide(this.toolbar.getElement());433} else {434dom.show(this.toolbar.getElement());435}436437if (data.vulns?.length && isResponseVM(data.element)) {438dom.clearNode(this.vulnsListElement);439this.element.classList.remove('no-vulns');440this.element.classList.toggle('chat-vulnerabilities-collapsed', !data.element.vulnerabilitiesListExpanded);441dom.append(this.vulnsListElement, ...data.vulns.map(v => $('li', undefined, $('span.chat-vuln-title', undefined, v.title), ' ' + v.description)));442this.vulnsButton.label = this.getVulnerabilitiesLabel();443} else {444this.element.classList.add('no-vulns');445}446}447448reset() {449this.clearWidgets();450this.currentCodeBlockData = undefined;451}452453private clearWidgets() {454ContentHoverController.get(this.editor)?.hideContentHover();455GlyphHoverController.get(this.editor)?.hideGlyphHover();456}457458private async updateEditor(data: ICodeBlockData): Promise<boolean> {459const textModel = await data.textModel;460if (this.isDisposed || this.currentCodeBlockData !== data || textModel.isDisposed()) {461return false;462}463464this.editor.setModel(textModel);465if (data.range) {466this.editor.setSelection(data.range);467this.editor.revealRangeInCenter(data.range, ScrollType.Immediate);468}469470this.updateContexts(data);471472return true;473}474475private getVulnerabilitiesLabel(): string {476if (!this.currentCodeBlockData || !this.currentCodeBlockData.vulns) {477return '';478}479480const referencesLabel = this.currentCodeBlockData.vulns.length > 1 ?481localize('vulnerabilitiesPlural', "{0} vulnerabilities", this.currentCodeBlockData.vulns.length) :482localize('vulnerabilitiesSingular', "{0} vulnerability", 1);483const icon = (element: IChatResponseViewModel) => element.vulnerabilitiesListExpanded ? Codicon.chevronDown : Codicon.chevronRight;484return `${referencesLabel} $(${icon(this.currentCodeBlockData.element as IChatResponseViewModel).id})`;485}486487private updateContexts(data: ICodeBlockData) {488const textModel = this.editor.getModel();489if (!textModel) {490return;491}492493this.toolbar.context = {494code: textModel.getTextBuffer().getValueInRange(data.range ?? textModel.getFullModelRange(), EndOfLinePreference.TextDefined),495codeBlockIndex: data.codeBlockIndex,496element: data.element,497languageId: textModel.getLanguageId(),498codemapperUri: data.codemapperUri,499chatSessionId: data.chatSessionId500} satisfies ICodeBlockActionContext;501this.resourceContextKey.set(textModel.uri);502}503}504505export class ChatCodeBlockContentProvider extends Disposable implements ITextModelContentProvider {506507constructor(508@ITextModelService textModelService: ITextModelService,509@IModelService private readonly _modelService: IModelService,510) {511super();512this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeChatCodeBlock, this));513}514515async provideTextContent(resource: URI): Promise<ITextModel | null> {516const existing = this._modelService.getModel(resource);517if (existing) {518return existing;519}520return this._modelService.createModel('', null, resource);521}522}523524//525526export interface ICodeCompareBlockActionContext {527readonly element: IChatResponseViewModel;528readonly diffEditor: IDiffEditor;529readonly edit: IChatTextEditGroup;530}531532export interface ICodeCompareBlockDiffData {533modified: ITextModel;534original: ITextModel;535originalSha1: string;536}537538export interface ICodeCompareBlockData {539readonly element: ChatTreeItem;540541readonly edit: IChatTextEditGroup;542543readonly diffData: Promise<ICodeCompareBlockDiffData | undefined>;544545readonly parentContextKeyService?: IContextKeyService;546// readonly hideToolbar?: boolean;547}548549550// long-lived object that sits in the DiffPool and that gets reused551export class CodeCompareBlockPart extends Disposable {552protected readonly _onDidChangeContentHeight = this._register(new Emitter<void>());553public readonly onDidChangeContentHeight = this._onDidChangeContentHeight.event;554555private readonly contextKeyService: IContextKeyService;556private readonly diffEditor: DiffEditorWidget;557private readonly resourceLabel: ResourceLabel;558private readonly toolbar: MenuWorkbenchToolBar;559readonly element: HTMLElement;560private readonly messageElement: HTMLElement;561562private readonly _lastDiffEditorViewModel = this._store.add(new MutableDisposable());563private currentScrollWidth = 0;564565constructor(566private readonly options: ChatEditorOptions,567readonly menuId: MenuId,568delegate: IChatRendererDelegate,569overflowWidgetsDomNode: HTMLElement | undefined,570private readonly isSimpleWidget: boolean = false,571@IInstantiationService instantiationService: IInstantiationService,572@IContextKeyService contextKeyService: IContextKeyService,573@IModelService protected readonly modelService: IModelService,574@IConfigurationService private readonly configurationService: IConfigurationService,575@IAccessibilityService private readonly accessibilityService: IAccessibilityService,576@ILabelService private readonly labelService: ILabelService,577@IOpenerService private readonly openerService: IOpenerService,578) {579super();580this.element = $('.interactive-result-code-block');581this.element.classList.add('compare');582583this.messageElement = dom.append(this.element, $('.message'));584this.messageElement.setAttribute('role', 'status');585this.messageElement.tabIndex = 0;586587this.contextKeyService = this._register(contextKeyService.createScoped(this.element));588const scopedInstantiationService = this._register(instantiationService.createChild(new ServiceCollection(589[IContextKeyService, this.contextKeyService],590[IEditorProgressService, new class implements IEditorProgressService {591_serviceBrand: undefined;592show(_total: unknown, _delay?: unknown) {593return emptyProgressRunner;594}595async showWhile(promise: Promise<unknown>, _delay?: number): Promise<void> {596await promise;597}598}],599)));600const editorHeader = dom.append(this.element, $('.interactive-result-header.show-file-icons'));601const editorElement = dom.append(this.element, $('.interactive-result-editor'));602this.diffEditor = this.createDiffEditor(scopedInstantiationService, editorElement, {603...getSimpleEditorOptions(this.configurationService),604lineNumbers: 'on',605selectOnLineNumbers: true,606scrollBeyondLastLine: false,607lineDecorationsWidth: 12,608dragAndDrop: false,609padding: { top: defaultCodeblockPadding, bottom: defaultCodeblockPadding },610mouseWheelZoom: false,611scrollbar: {612vertical: 'hidden',613alwaysConsumeMouseWheel: false614},615definitionLinkOpensInPeek: false,616gotoLocation: {617multiple: 'goto',618multipleDeclarations: 'goto',619multipleDefinitions: 'goto',620multipleImplementations: 'goto',621},622ariaLabel: localize('chat.codeBlockHelp', 'Code block'),623overflowWidgetsDomNode,624...this.getEditorOptionsFromConfig(),625});626627this.resourceLabel = this._register(scopedInstantiationService.createInstance(ResourceLabel, editorHeader, { supportIcons: true }));628629const editorScopedService = this.diffEditor.getModifiedEditor().contextKeyService.createScoped(editorHeader);630const editorScopedInstantiationService = this._register(scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService])));631this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, editorHeader, menuId, {632menuOptions: {633shouldForwardArgs: true634}635}));636637this._configureForScreenReader();638this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this._configureForScreenReader()));639this._register(this.configurationService.onDidChangeConfiguration((e) => {640if (e.affectedKeys.has(AccessibilityVerbositySettingId.Chat)) {641this._configureForScreenReader();642}643}));644645this._register(this.options.onDidChange(() => {646this.diffEditor.updateOptions(this.getEditorOptionsFromConfig());647}));648649this._register(this.diffEditor.getModifiedEditor().onDidScrollChange(e => {650this.currentScrollWidth = e.scrollWidth;651}));652this._register(this.diffEditor.onDidContentSizeChange(e => {653if (e.contentHeightChanged) {654this._onDidChangeContentHeight.fire();655}656}));657this._register(this.diffEditor.getModifiedEditor().onDidBlurEditorWidget(() => {658this.element.classList.remove('focused');659WordHighlighterContribution.get(this.diffEditor.getModifiedEditor())?.stopHighlighting();660this.clearWidgets();661}));662this._register(this.diffEditor.getModifiedEditor().onDidFocusEditorWidget(() => {663this.element.classList.add('focused');664WordHighlighterContribution.get(this.diffEditor.getModifiedEditor())?.restoreViewState(true);665}));666667668// Parent list scrolled669if (delegate.onDidScroll) {670this._register(delegate.onDidScroll(e => {671this.clearWidgets();672}));673}674}675676get uri(): URI | undefined {677return this.diffEditor.getModifiedEditor().getModel()?.uri;678}679680private createDiffEditor(instantiationService: IInstantiationService, parent: HTMLElement, options: Readonly<IEditorConstructionOptions>): DiffEditorWidget {681const widgetOptions: ICodeEditorWidgetOptions = {682isSimpleWidget: this.isSimpleWidget,683contributions: EditorExtensionsRegistry.getSomeEditorContributions([684MenuPreventer.ID,685SelectionClipboardContributionID,686ContextMenuController.ID,687688WordHighlighterContribution.ID,689ViewportSemanticTokensContribution.ID,690BracketMatchingController.ID,691SmartSelectController.ID,692ContentHoverController.ID,693GlyphHoverController.ID,694GotoDefinitionAtPositionEditorContribution.ID,695])696};697698return this._register(instantiationService.createInstance(DiffEditorWidget, parent, {699scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false, ignoreHorizontalScrollbarInContentHeight: true, },700renderMarginRevertIcon: false,701diffCodeLens: false,702scrollBeyondLastLine: false,703stickyScroll: { enabled: false },704originalAriaLabel: localize('original', 'Original'),705modifiedAriaLabel: localize('modified', 'Modified'),706diffAlgorithm: 'advanced',707readOnly: false,708isInEmbeddedEditor: true,709useInlineViewWhenSpaceIsLimited: true,710experimental: {711useTrueInlineView: true,712},713renderSideBySideInlineBreakpoint: 300,714renderOverviewRuler: false,715compactMode: true,716hideUnchangedRegions: { enabled: true, contextLineCount: 1 },717renderGutterMenu: false,718lineNumbersMinChars: 1,719...options720}, { originalEditor: widgetOptions, modifiedEditor: widgetOptions }));721}722723focus(): void {724this.diffEditor.focus();725}726727private updatePaddingForLayout() {728// scrollWidth = "the width of the content that needs to be scrolled"729// contentWidth = "the width of the area where content is displayed"730const horizontalScrollbarVisible = this.currentScrollWidth > this.diffEditor.getModifiedEditor().getLayoutInfo().contentWidth;731const scrollbarHeight = this.diffEditor.getModifiedEditor().getLayoutInfo().horizontalScrollbarHeight;732const bottomPadding = horizontalScrollbarVisible ?733Math.max(defaultCodeblockPadding - scrollbarHeight, 2) :734defaultCodeblockPadding;735this.diffEditor.updateOptions({ padding: { top: defaultCodeblockPadding, bottom: bottomPadding } });736}737738private _configureForScreenReader(): void {739const toolbarElt = this.toolbar.getElement();740if (this.accessibilityService.isScreenReaderOptimized()) {741toolbarElt.style.display = 'block';742toolbarElt.ariaLabel = localize('chat.codeBlock.toolbar', 'Code block toolbar');743} else {744toolbarElt.style.display = '';745}746}747748private getEditorOptionsFromConfig(): IEditorOptions {749return {750wordWrap: this.options.configuration.resultEditor.wordWrap,751fontLigatures: this.options.configuration.resultEditor.fontLigatures,752bracketPairColorization: this.options.configuration.resultEditor.bracketPairColorization,753fontFamily: this.options.configuration.resultEditor.fontFamily === 'default' ?754EDITOR_FONT_DEFAULTS.fontFamily :755this.options.configuration.resultEditor.fontFamily,756fontSize: this.options.configuration.resultEditor.fontSize,757fontWeight: this.options.configuration.resultEditor.fontWeight,758lineHeight: this.options.configuration.resultEditor.lineHeight,759};760}761762layout(width: number): void {763const editorBorder = 2;764765const toolbar = dom.getTotalHeight(this.toolbar.getElement());766const content = this.diffEditor.getModel()767? this.diffEditor.getContentHeight()768: dom.getTotalHeight(this.messageElement);769770const dimension = new dom.Dimension(width - editorBorder, toolbar + content);771this.element.style.height = `${dimension.height}px`;772this.element.style.width = `${dimension.width}px`;773this.diffEditor.layout(dimension.with(undefined, content - editorBorder));774this.updatePaddingForLayout();775}776777778async render(data: ICodeCompareBlockData, width: number, token: CancellationToken) {779if (data.parentContextKeyService) {780this.contextKeyService.updateParent(data.parentContextKeyService);781}782783if (this.options.configuration.resultEditor.wordWrap === 'on') {784// Initialize the editor with the new proper width so that getContentHeight785// will be computed correctly in the next call to layout()786this.layout(width);787}788789await this.updateEditor(data, token);790791this.layout(width);792this.diffEditor.updateOptions({ ariaLabel: localize('chat.compareCodeBlockLabel', "Code Edits") });793794this.resourceLabel.element.setFile(data.edit.uri, {795fileKind: FileKind.FILE,796fileDecorations: { colors: true, badges: false }797});798}799800reset() {801this.clearWidgets();802}803804private clearWidgets() {805ContentHoverController.get(this.diffEditor.getOriginalEditor())?.hideContentHover();806ContentHoverController.get(this.diffEditor.getModifiedEditor())?.hideContentHover();807GlyphHoverController.get(this.diffEditor.getOriginalEditor())?.hideGlyphHover();808GlyphHoverController.get(this.diffEditor.getModifiedEditor())?.hideGlyphHover();809}810811private async updateEditor(data: ICodeCompareBlockData, token: CancellationToken): Promise<void> {812813if (!isResponseVM(data.element)) {814return;815}816817const isEditApplied = Boolean(data.edit.state?.applied ?? 0);818819ChatContextKeys.editApplied.bindTo(this.contextKeyService).set(isEditApplied);820821this.element.classList.toggle('no-diff', isEditApplied);822823if (isEditApplied) {824assertType(data.edit.state?.applied);825826const uriLabel = this.labelService.getUriLabel(data.edit.uri, { relative: true, noPrefix: true });827828let template: string;829if (data.edit.state.applied === 1) {830template = localize('chat.edits.1', "Applied 1 change in [[``{0}``]]", uriLabel);831} else if (data.edit.state.applied < 0) {832template = localize('chat.edits.rejected', "Edits in [[``{0}``]] have been rejected", uriLabel);833} else {834template = localize('chat.edits.N', "Applied {0} changes in [[``{1}``]]", data.edit.state.applied, uriLabel);835}836837const message = renderFormattedText(template, {838renderCodeSegments: true,839actionHandler: {840callback: () => {841this.openerService.open(data.edit.uri, { fromUserGesture: true, allowCommands: false });842},843disposables: this._store,844}845});846847dom.reset(this.messageElement, message);848}849850const diffData = await data.diffData;851852if (!isEditApplied && diffData) {853const viewModel = this.diffEditor.createViewModel({854original: diffData.original,855modified: diffData.modified856});857858await viewModel.waitForDiff();859860if (token.isCancellationRequested) {861return;862}863864const listener = Event.any(diffData.original.onWillDispose, diffData.modified.onWillDispose)(() => {865// this a bit weird and basically duplicates https://github.com/microsoft/vscode/blob/7cbcafcbcc88298cfdcd0238018fbbba8eb6853e/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts#L328866// which cannot call `setModel(null)` without first complaining867this.diffEditor.setModel(null);868});869this.diffEditor.setModel(viewModel);870this._lastDiffEditorViewModel.value = combinedDisposable(listener, viewModel);871872} else {873this.diffEditor.setModel(null);874this._lastDiffEditorViewModel.value = undefined;875this._onDidChangeContentHeight.fire();876}877878this.toolbar.context = {879edit: data.edit,880element: data.element,881diffEditor: this.diffEditor,882} satisfies ICodeCompareBlockActionContext;883}884}885886export class DefaultChatTextEditor {887888private readonly _sha1 = new DefaultModelSHA1Computer();889890constructor(891@ITextModelService private readonly modelService: ITextModelService,892@ICodeEditorService private readonly editorService: ICodeEditorService,893@IDialogService private readonly dialogService: IDialogService,894) { }895896async apply(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup, diffEditor: IDiffEditor | undefined): Promise<void> {897898if (!response.response.value.includes(item)) {899// bogous item900return;901}902903if (item.state?.applied) {904// already applied905return;906}907908if (!diffEditor) {909for (const candidate of this.editorService.listDiffEditors()) {910if (!candidate.getContainerDomNode().isConnected) {911continue;912}913const model = candidate.getModel();914if (!model || !isEqual(model.original.uri, item.uri) || model.modified.uri.scheme !== Schemas.vscodeChatCodeCompareBlock) {915diffEditor = candidate;916break;917}918}919}920921const edits = diffEditor922? await this._applyWithDiffEditor(diffEditor, item)923: await this._apply(item);924925response.setEditApplied(item, edits);926}927928private async _applyWithDiffEditor(diffEditor: IDiffEditor, item: IChatTextEditGroup) {929const model = diffEditor.getModel();930if (!model) {931return 0;932}933934const diff = diffEditor.getDiffComputationResult();935if (!diff || diff.identical) {936return 0;937}938939940if (!await this._checkSha1(model.original, item)) {941return 0;942}943944const modified = new TextModelText(model.modified);945const edits = diff.changes2.map(i => i.toRangeMapping().toTextEdit(modified).toSingleEditOperation());946947model.original.pushStackElement();948model.original.pushEditOperations(null, edits, () => null);949model.original.pushStackElement();950951return edits.length;952}953954private async _apply(item: IChatTextEditGroup) {955const ref = await this.modelService.createModelReference(item.uri);956try {957958if (!await this._checkSha1(ref.object.textEditorModel, item)) {959return 0;960}961962ref.object.textEditorModel.pushStackElement();963let total = 0;964for (const group of item.edits) {965const edits = group.map(TextEdit.asEditOperation);966ref.object.textEditorModel.pushEditOperations(null, edits, () => null);967total += edits.length;968}969ref.object.textEditorModel.pushStackElement();970return total;971972} finally {973ref.dispose();974}975}976977private async _checkSha1(model: ITextModel, item: IChatTextEditGroup) {978if (item.state?.sha1 && this._sha1.computeSHA1(model) && this._sha1.computeSHA1(model) !== item.state.sha1) {979const result = await this.dialogService.confirm({980message: localize('interactive.compare.apply.confirm', "The original file has been modified."),981detail: localize('interactive.compare.apply.confirm.detail', "Do you want to apply the changes anyway?"),982});983984if (!result.confirmed) {985return false;986}987}988return true;989}990991discard(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup) {992if (!response.response.value.includes(item)) {993// bogous item994return;995}996997if (item.state?.applied) {998// already applied999return;1000}10011002response.setEditApplied(item, -1);1003}100410051006}100710081009