Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts
5251 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 { AsyncIterableProducer } from '../../../../../base/common/async.js';6import { CancellationToken } from '../../../../../base/common/cancellation.js';7import { Codicon } from '../../../../../base/common/codicons.js';8import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';9import { Disposable, markAsSingleton } from '../../../../../base/common/lifecycle.js';10import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';11import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';12import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';13import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';14import { CopyAction } from '../../../../../editor/contrib/clipboard/browser/clipboard.js';15import { localize, localize2 } from '../../../../../nls.js';16import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';17import { MenuEntryActionViewItem } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';18import { Action2, MenuId, MenuItemAction, registerAction2 } from '../../../../../platform/actions/common/actions.js';19import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';20import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';21import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';22import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';23import { ILabelService } from '../../../../../platform/label/common/label.js';24import { TerminalLocation } from '../../../../../platform/terminal/common/terminal.js';25import { IWorkbenchContribution } from '../../../../common/contributions.js';26import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js';27import { IEditorService } from '../../../../services/editor/common/editorService.js';28import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js';29import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';30import { EditDeltaInfo } from '../../../../../editor/common/textModelEditSource.js';31import { reviewEdits } from '../../../inlineChat/browser/inlineChatController.js';32import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js';33import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';34import { ChatCopyKind, IChatService } from '../../common/chatService/chatService.js';35import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';36import { ChatAgentLocation } from '../../common/constants.js';37import { IChatCodeBlockContextProviderService, IChatWidgetService } from '../chat.js';38import { DefaultChatTextEditor, ICodeBlockActionContext, ICodeCompareBlockActionContext } from '../widget/chatContentParts/codeBlockPart.js';39import { CHAT_CATEGORY } from './chatActions.js';40import { ApplyCodeBlockOperation, InsertCodeBlockOperation } from './codeBlockOperations.js';4142const shellLangIds = [43'fish',44'ps1',45'pwsh',46'powershell',47'sh',48'shellscript',49'zsh'50];5152export interface IChatCodeBlockActionContext extends ICodeBlockActionContext {53element: IChatResponseViewModel;54}5556export function isCodeBlockActionContext(thing: unknown): thing is ICodeBlockActionContext {57return typeof thing === 'object' && thing !== null && 'code' in thing && 'element' in thing;58}5960export function isCodeCompareBlockActionContext(thing: unknown): thing is ICodeCompareBlockActionContext {61return typeof thing === 'object' && thing !== null && 'element' in thing && 'diffEditor' in thing && 'toggleDiffViewMode' in thing;62}6364function isResponseFiltered(context: ICodeBlockActionContext) {65return isResponseVM(context.element) && context.element.errorDetails?.responseIsFiltered;66}6768abstract class ChatCodeBlockAction extends Action2 {69run(accessor: ServicesAccessor, ...args: unknown[]) {70let context = args[0];71if (!isCodeBlockActionContext(context)) {72const codeEditorService = accessor.get(ICodeEditorService);73const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor();74if (!editor) {75return;76}7778context = getContextFromEditor(editor, accessor);79if (!isCodeBlockActionContext(context)) {80return;81}82}8384return this.runWithContext(accessor, context);85}8687// eslint-disable-next-line @typescript-eslint/no-explicit-any88abstract runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext): any;89}9091const APPLY_IN_EDITOR_ID = 'workbench.action.chat.applyInEditor';9293export class CodeBlockActionRendering extends Disposable implements IWorkbenchContribution {9495static readonly ID = 'chat.codeBlockActionRendering';9697constructor(98@IActionViewItemService actionViewItemService: IActionViewItemService,99@IInstantiationService instantiationService: IInstantiationService,100@ILabelService labelService: ILabelService,101) {102super();103104const disposable = actionViewItemService.register(MenuId.ChatCodeBlock, APPLY_IN_EDITOR_ID, (action, options) => {105if (!(action instanceof MenuItemAction)) {106return undefined;107}108return instantiationService.createInstance(class extends MenuEntryActionViewItem {109protected override getTooltip(): string {110const context = this._context;111if (isCodeBlockActionContext(context) && context.codemapperUri) {112const label = labelService.getUriLabel(context.codemapperUri, { relative: true });113return localize('interactive.applyInEditorWithURL.label', "Apply to {0}", label);114}115return super.getTooltip();116}117override setActionContext(newContext: unknown): void {118super.setActionContext(newContext);119this.updateTooltip();120}121}, action, undefined);122});123124// Reduces flicker a bit on reload/restart125markAsSingleton(disposable);126}127}128129export function registerChatCodeBlockActions() {130registerAction2(class CopyCodeBlockAction extends Action2 {131constructor() {132super({133id: 'workbench.action.chat.copyCodeBlock',134title: localize2('interactive.copyCodeBlock.label', "Copy"),135f1: false,136category: CHAT_CATEGORY,137icon: Codicon.copy,138menu: {139id: MenuId.ChatCodeBlock,140group: 'navigation',141order: 30142}143});144}145146run(accessor: ServicesAccessor, ...args: unknown[]) {147const context = args[0];148if (!isCodeBlockActionContext(context) || isResponseFiltered(context)) {149return;150}151152const clipboardService = accessor.get(IClipboardService);153const aiEditTelemetryService = accessor.get(IAiEditTelemetryService);154clipboardService.writeText(context.code);155156if (isResponseVM(context.element)) {157const chatService = accessor.get(IChatService);158const requestId = context.element.requestId;159const request = context.element.session.getItems().find(item => item.id === requestId && isRequestVM(item)) as IChatRequestViewModel | undefined;160chatService.notifyUserAction({161agentId: context.element.agent?.id,162command: context.element.slashCommand?.name,163sessionResource: context.element.sessionResource,164requestId: context.element.requestId,165result: context.element.result,166action: {167kind: 'copy',168codeBlockIndex: context.codeBlockIndex,169copyKind: ChatCopyKind.Toolbar,170copiedCharacters: context.code.length,171totalCharacters: context.code.length,172copiedText: context.code,173copiedLines: context.code.split('\n').length,174languageId: context.languageId,175totalLines: context.code.split('\n').length,176modelId: request?.modelId ?? ''177}178});179180const codeBlockInfo = context.element.model.codeBlockInfos?.at(context.codeBlockIndex);181aiEditTelemetryService.handleCodeAccepted({182acceptanceMethod: 'copyButton',183suggestionId: codeBlockInfo?.suggestionId,184editDeltaInfo: EditDeltaInfo.fromText(context.code),185feature: 'sideBarChat',186languageId: context.languageId,187modeId: context.element.model.request?.modeInfo?.modeId,188modelId: request?.modelId,189presentation: 'codeBlock',190applyCodeBlockSuggestionId: undefined,191source: undefined,192});193}194}195});196197CopyAction?.addImplementation(50000, 'chat-codeblock', (accessor) => {198// get active code editor199const editor = accessor.get(ICodeEditorService).getFocusedCodeEditor();200if (!editor) {201return false;202}203204const editorModel = editor.getModel();205if (!editorModel) {206return false;207}208209const context = getContextFromEditor(editor, accessor);210if (!context) {211return false;212}213214const noSelection = editor.getSelections()?.length === 1 && editor.getSelection()?.isEmpty();215const copiedText = noSelection ?216editorModel.getValue() :217editor.getSelections()?.reduce((acc, selection) => acc + editorModel.getValueInRange(selection), '') ?? '';218const totalCharacters = editorModel.getValueLength();219220// Report copy to extensions221const chatService = accessor.get(IChatService);222const aiEditTelemetryService = accessor.get(IAiEditTelemetryService);223const element = context.element as IChatResponseViewModel | undefined;224if (isResponseVM(element)) {225const requestId = element.requestId;226const request = element.session.getItems().find(item => item.id === requestId && isRequestVM(item)) as IChatRequestViewModel | undefined;227chatService.notifyUserAction({228agentId: element.agent?.id,229command: element.slashCommand?.name,230sessionResource: element.sessionResource,231requestId: element.requestId,232result: element.result,233action: {234kind: 'copy',235codeBlockIndex: context.codeBlockIndex,236copyKind: ChatCopyKind.Action,237copiedText,238copiedCharacters: copiedText.length,239totalCharacters,240languageId: context.languageId,241totalLines: context.code.split('\n').length,242copiedLines: copiedText.split('\n').length,243modelId: request?.modelId ?? ''244}245});246247const codeBlockInfo = element.model.codeBlockInfos?.at(context.codeBlockIndex);248aiEditTelemetryService.handleCodeAccepted({249acceptanceMethod: 'copyManual',250suggestionId: codeBlockInfo?.suggestionId,251editDeltaInfo: EditDeltaInfo.fromText(copiedText),252feature: 'sideBarChat',253languageId: context.languageId,254modeId: element.model.request?.modeInfo?.modeId,255modelId: request?.modelId,256presentation: 'codeBlock',257applyCodeBlockSuggestionId: undefined,258source: undefined,259});260}261262// Copy full cell if no selection, otherwise fall back on normal editor implementation263if (noSelection) {264accessor.get(IClipboardService).writeText(context.code);265return true;266}267268return false;269});270271registerAction2(class SmartApplyInEditorAction extends ChatCodeBlockAction {272273private operation: ApplyCodeBlockOperation | undefined;274275constructor() {276super({277id: APPLY_IN_EDITOR_ID,278title: localize2('interactive.applyInEditor.label', "Apply in Editor"),279precondition: ChatContextKeys.enabled,280f1: false,281category: CHAT_CATEGORY,282icon: Codicon.gitPullRequestGoToChanges,283284menu: [285{286id: MenuId.ChatCodeBlock,287group: 'navigation',288when: ContextKeyExpr.and(289...shellLangIds.map(e => ContextKeyExpr.notEquals(EditorContextKeys.languageId.key, e))290),291order: 10292},293{294id: MenuId.ChatCodeBlock,295when: ContextKeyExpr.or(296...shellLangIds.map(e => ContextKeyExpr.equals(EditorContextKeys.languageId.key, e))297)298},299],300keybinding: {301when: ContextKeyExpr.or(ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate()), accessibleViewInCodeBlock),302primary: KeyMod.CtrlCmd | KeyCode.Enter,303mac: { primary: KeyMod.WinCtrl | KeyCode.Enter },304weight: KeybindingWeight.ExternalExtension + 1305},306});307}308309override runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) {310if (!this.operation) {311this.operation = accessor.get(IInstantiationService).createInstance(ApplyCodeBlockOperation);312}313return this.operation.run(context);314}315});316317registerAction2(class InsertAtCursorAction extends ChatCodeBlockAction {318constructor() {319super({320id: 'workbench.action.chat.insertCodeBlock',321title: localize2('interactive.insertCodeBlock.label', "Insert At Cursor"),322precondition: ChatContextKeys.enabled,323f1: true,324category: CHAT_CATEGORY,325icon: Codicon.insert,326menu: [{327id: MenuId.ChatCodeBlock,328group: 'navigation',329when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.location.notEqualsTo(ChatAgentLocation.Terminal)),330order: 20331}, {332id: MenuId.ChatCodeBlock,333group: 'navigation',334when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Terminal)),335isHiddenByDefault: true,336order: 20337}],338keybinding: {339when: ContextKeyExpr.or(ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate()), accessibleViewInCodeBlock),340primary: KeyMod.CtrlCmd | KeyCode.Enter,341mac: { primary: KeyMod.WinCtrl | KeyCode.Enter },342weight: KeybindingWeight.ExternalExtension + 1343},344});345}346347override runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) {348const operation = accessor.get(IInstantiationService).createInstance(InsertCodeBlockOperation);349return operation.run(context);350}351});352353registerAction2(class InsertIntoNewFileAction extends ChatCodeBlockAction {354constructor() {355super({356id: 'workbench.action.chat.insertIntoNewFile',357title: localize2('interactive.insertIntoNewFile.label', "Insert into New File"),358precondition: ChatContextKeys.enabled,359f1: true,360category: CHAT_CATEGORY,361icon: Codicon.newFile,362menu: {363id: MenuId.ChatCodeBlock,364group: 'navigation',365isHiddenByDefault: true,366order: 40,367}368});369}370371override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) {372if (isResponseFiltered(context)) {373// When run from command palette374return;375}376377const editorService = accessor.get(IEditorService);378const chatService = accessor.get(IChatService);379const aiEditTelemetryService = accessor.get(IAiEditTelemetryService);380381editorService.openEditor({ contents: context.code, languageId: context.languageId, resource: undefined } satisfies IUntitledTextResourceEditorInput);382383if (isResponseVM(context.element)) {384const requestId = context.element.requestId;385const request = context.element.session.getItems().find(item => item.id === requestId && isRequestVM(item)) as IChatRequestViewModel | undefined;386chatService.notifyUserAction({387agentId: context.element.agent?.id,388command: context.element.slashCommand?.name,389sessionResource: context.element.sessionResource,390requestId: context.element.requestId,391result: context.element.result,392action: {393kind: 'insert',394codeBlockIndex: context.codeBlockIndex,395totalCharacters: context.code.length,396newFile: true,397totalLines: context.code.split('\n').length,398languageId: context.languageId,399modelId: request?.modelId ?? ''400}401});402403const codeBlockInfo = context.element.model.codeBlockInfos?.at(context.codeBlockIndex);404405aiEditTelemetryService.handleCodeAccepted({406acceptanceMethod: 'insertInNewFile',407suggestionId: codeBlockInfo?.suggestionId,408editDeltaInfo: EditDeltaInfo.fromText(context.code),409feature: 'sideBarChat',410languageId: context.languageId,411modeId: context.element.model.request?.modeInfo?.modeId,412modelId: request?.modelId,413presentation: 'codeBlock',414applyCodeBlockSuggestionId: undefined,415source: undefined,416});417}418}419});420421registerAction2(class RunInTerminalAction extends ChatCodeBlockAction {422constructor() {423super({424id: 'workbench.action.chat.runInTerminal',425title: localize2('interactive.runInTerminal.label', "Insert into Terminal"),426precondition: ChatContextKeys.enabled,427f1: true,428category: CHAT_CATEGORY,429icon: Codicon.terminal,430menu: [{431id: MenuId.ChatCodeBlock,432group: 'navigation',433when: ContextKeyExpr.and(434ChatContextKeys.inChatSession,435ContextKeyExpr.or(...shellLangIds.map(e => ContextKeyExpr.equals(EditorContextKeys.languageId.key, e)))436),437},438{439id: MenuId.ChatCodeBlock,440group: 'navigation',441isHiddenByDefault: true,442when: ContextKeyExpr.and(443ChatContextKeys.inChatSession,444...shellLangIds.map(e => ContextKeyExpr.notEquals(EditorContextKeys.languageId.key, e))445)446}],447keybinding: [{448primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter,449mac: {450primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter451},452weight: KeybindingWeight.EditorContrib,453when: ContextKeyExpr.or(ChatContextKeys.inChatSession, accessibleViewInCodeBlock),454}]455});456}457458override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) {459if (isResponseFiltered(context)) {460// When run from command palette461return;462}463464const chatService = accessor.get(IChatService);465const terminalService = accessor.get(ITerminalService);466const editorService = accessor.get(IEditorService);467const terminalEditorService = accessor.get(ITerminalEditorService);468const terminalGroupService = accessor.get(ITerminalGroupService);469470let terminal = await terminalService.getActiveOrCreateInstance({ acceptsInput: true });471472// isFeatureTerminal = debug terminal or task terminal473if (terminal.xterm?.isStdinDisabled || terminal.shellLaunchConfig.isFeatureTerminal) {474terminal = await terminalService.createAndFocusTerminal({ location: TerminalLocation.Panel });475} else {476await terminalService.focusInstance(terminal);477}478479if (terminal.target === TerminalLocation.Editor) {480const existingEditors = editorService.findEditors(terminal.resource);481terminalEditorService.openEditor(terminal, { viewColumn: existingEditors?.[0].groupId });482} else {483await terminalGroupService.showPanel(true);484}485486terminal.runCommand(context.code, false);487488if (isResponseVM(context.element)) {489chatService.notifyUserAction({490agentId: context.element.agent?.id,491command: context.element.slashCommand?.name,492sessionResource: context.element.sessionResource,493requestId: context.element.requestId,494result: context.element.result,495action: {496kind: 'runInTerminal',497codeBlockIndex: context.codeBlockIndex,498languageId: context.languageId,499}500});501}502}503});504505function navigateCodeBlocks(accessor: ServicesAccessor, reverse?: boolean): void {506const codeEditorService = accessor.get(ICodeEditorService);507const chatWidgetService = accessor.get(IChatWidgetService);508const widget = chatWidgetService.lastFocusedWidget;509if (!widget) {510return;511}512513const editor = codeEditorService.getFocusedCodeEditor();514const editorUri = editor?.getModel()?.uri;515const curCodeBlockInfo = editorUri ? widget.getCodeBlockInfoForEditor(editorUri) : undefined;516const focused = !widget.inputEditor.hasWidgetFocus() && widget.getFocus();517const focusedResponse = isResponseVM(focused) ? focused : undefined;518519const elementId = curCodeBlockInfo?.elementId;520const element = elementId ? widget.viewModel?.getItems().find(item => item.id === elementId) : undefined;521const currentResponse = element ??522(focusedResponse ?? widget.viewModel?.getItems().reverse().find((item): item is IChatResponseViewModel => isResponseVM(item)));523if (!currentResponse || !isResponseVM(currentResponse)) {524return;525}526527widget.reveal(currentResponse);528const responseCodeblocks = widget.getCodeBlockInfosForResponse(currentResponse);529const focusIdx = curCodeBlockInfo ?530(curCodeBlockInfo.codeBlockIndex + (reverse ? -1 : 1) + responseCodeblocks.length) % responseCodeblocks.length :531reverse ? responseCodeblocks.length - 1 : 0;532533responseCodeblocks[focusIdx]?.focus();534}535536registerAction2(class NextCodeBlockAction extends Action2 {537constructor() {538super({539id: 'workbench.action.chat.nextCodeBlock',540title: localize2('interactive.nextCodeBlock.label', "Next Code Block"),541keybinding: {542primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown,543mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, },544weight: KeybindingWeight.WorkbenchContrib,545when: ChatContextKeys.inChatSession,546},547precondition: ChatContextKeys.enabled,548f1: true,549category: CHAT_CATEGORY,550});551}552553run(accessor: ServicesAccessor, ...args: unknown[]) {554navigateCodeBlocks(accessor);555}556});557558registerAction2(class PreviousCodeBlockAction extends Action2 {559constructor() {560super({561id: 'workbench.action.chat.previousCodeBlock',562title: localize2('interactive.previousCodeBlock.label', "Previous Code Block"),563keybinding: {564primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp,565mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp, },566weight: KeybindingWeight.WorkbenchContrib,567when: ChatContextKeys.inChatSession,568},569precondition: ChatContextKeys.enabled,570f1: true,571category: CHAT_CATEGORY,572});573}574575run(accessor: ServicesAccessor, ...args: unknown[]) {576navigateCodeBlocks(accessor, true);577}578});579}580581function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): ICodeBlockActionContext | undefined {582const chatWidgetService = accessor.get(IChatWidgetService);583const chatCodeBlockContextProviderService = accessor.get(IChatCodeBlockContextProviderService);584const model = editor.getModel();585if (!model) {586return;587}588589const widget = chatWidgetService.lastFocusedWidget;590const codeBlockInfo = widget?.getCodeBlockInfoForEditor(model.uri);591if (!codeBlockInfo) {592for (const provider of chatCodeBlockContextProviderService.providers) {593const context = provider.getCodeBlockContext(editor);594if (context) {595return context;596}597}598return;599}600601const element = widget?.viewModel?.getItems().find(item => item.id === codeBlockInfo.elementId);602return {603element,604codeBlockIndex: codeBlockInfo.codeBlockIndex,605code: editor.getValue(),606languageId: editor.getModel()!.getLanguageId(),607codemapperUri: codeBlockInfo.codemapperUri,608chatSessionResource: codeBlockInfo.chatSessionResource,609};610}611612export function registerChatCodeCompareBlockActions() {613614abstract class ChatCompareCodeBlockAction extends Action2 {615run(accessor: ServicesAccessor, ...args: unknown[]) {616const context = args[0];617if (!isCodeCompareBlockActionContext(context)) {618return;619// TODO@jrieken derive context620}621622return this.runWithContext(accessor, context);623}624625// eslint-disable-next-line @typescript-eslint/no-explicit-any626abstract runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): any;627}628629registerAction2(class ApplyEditsCompareBlockAction extends ChatCompareCodeBlockAction {630constructor() {631super({632id: 'workbench.action.chat.applyCompareEdits',633title: localize2('interactive.compare.apply', "Apply Edits"),634f1: false,635category: CHAT_CATEGORY,636icon: Codicon.gitPullRequestGoToChanges,637precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, ChatContextKeys.editApplied.negate(), EditorContextKeys.readOnly.negate()),638menu: {639id: MenuId.ChatCompareBlock,640group: 'navigation',641order: 10,642when: EditorContextKeys.readOnly.negate(),643}644});645}646647// eslint-disable-next-line @typescript-eslint/no-explicit-any648async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise<any> {649650const instaService = accessor.get(IInstantiationService);651const editorService = accessor.get(ICodeEditorService);652653const item = context.edit;654const response = context.element;655656if (item.state?.applied) {657// already applied658return false;659}660661if (!response.response.value.includes(item)) {662// bogous item663return false;664}665666const firstEdit = item.edits[0]?.[0];667if (!firstEdit) {668return false;669}670const textEdits = AsyncIterableProducer.fromArray(item.edits);671672const editorToApply = await editorService.openCodeEditor({ resource: item.uri }, null);673if (editorToApply) {674editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber);675instaService.invokeFunction(reviewEdits, editorToApply, textEdits, CancellationToken.None, undefined);676response.setEditApplied(item, 1);677return true;678}679return false;680}681});682683registerAction2(class DiscardEditsCompareBlockAction extends ChatCompareCodeBlockAction {684constructor() {685super({686id: 'workbench.action.chat.discardCompareEdits',687title: localize2('interactive.compare.discard', "Discard Edits"),688f1: false,689category: CHAT_CATEGORY,690icon: Codicon.trash,691precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, ChatContextKeys.editApplied.negate(), EditorContextKeys.readOnly.negate()),692menu: {693id: MenuId.ChatCompareBlock,694group: 'navigation',695order: 11,696when: EditorContextKeys.readOnly.negate(),697}698});699}700701// eslint-disable-next-line @typescript-eslint/no-explicit-any702async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise<any> {703const instaService = accessor.get(IInstantiationService);704const editor = instaService.createInstance(DefaultChatTextEditor);705editor.discard(context.element, context.edit);706}707});708709registerAction2(class ToggleDiffViewModeAction extends ChatCompareCodeBlockAction {710constructor() {711super({712id: 'workbench.action.chat.toggleCompareBlockDiffViewMode',713title: localize2('interactive.compare.toggleDiffViewMode', "Toggle Inline/Side-by-Side Diff"),714f1: false,715category: CHAT_CATEGORY,716icon: Codicon.diffSingle,717toggled: {718condition: EditorContextKeys.diffEditorInlineMode,719icon: Codicon.diff,720},721menu: {722id: MenuId.ChatCompareBlock,723group: 'navigation',724order: 1,725}726});727}728729runWithContext(_accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): void {730context.toggleDiffViewMode();731}732});733734registerAction2(class OpenCompareBlockInDiffEditor extends ChatCompareCodeBlockAction {735constructor() {736super({737id: 'workbench.action.chat.openCompareBlockInDiffEditor',738title: localize2('interactive.compare.openInDiffEditor', "Open in Diff Editor"),739f1: false,740category: CHAT_CATEGORY,741icon: Codicon.goToFile,742menu: {743id: MenuId.ChatCompareBlock,744group: 'navigation',745order: 2,746}747});748}749750async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise<void> {751const editorService = accessor.get(IEditorService);752const model = context.diffEditor.getModel();753if (!model) {754return;755}756757await editorService.openEditor({758original: { resource: model.original.uri },759modified: { resource: model.modified.uri },760});761}762});763}764765766