diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index b896a99bf9b..973c1072b1a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -30,6 +30,7 @@ registerAction2(InlineChatActions.UnstashSessionAction); registerAction2(InlineChatActions.MakeRequestAction); registerAction2(InlineChatActions.StopRequestAction); registerAction2(InlineChatActions.ReRunRequestAction); +registerAction2(InlineChatActions.DiscardHunkAction); registerAction2(InlineChatActions.DiscardAction); registerAction2(InlineChatActions.DiscardToClipboardAction); registerAction2(InlineChatActions.DiscardUndoToNewFileAction); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 337fcaaf551..ecca297e868 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -303,6 +303,29 @@ export class NextFromHistory extends AbstractInlineChatAction { } } +export class DiscardHunkAction extends AbstractInlineChatAction { + + constructor() { + super({ + id: 'inlineChat.discardHunkChange', + title: localize('discard', 'Discard'), + icon: Codicon.clearAll, + precondition: CTX_INLINE_CHAT_VISIBLE, + menu: { + id: MENU_INLINE_CHAT_WIDGET_STATUS, + when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live)), + group: '0_main', + order: 3 + } + }); + } + + async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { + return ctrl.discardHunk(); + } +} + + MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, { submenu: MENU_INLINE_CHAT_WIDGET_DISCARD, title: localize('discardMenu', "Discard..."), @@ -510,7 +533,7 @@ export class AcceptChanges extends AbstractInlineChatAction { when: CTX_INLINE_CHAT_USER_DID_EDIT }], menu: { - when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Live)), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.OnlyMessages)), id: MENU_INLINE_CHAT_WIDGET_STATUS, group: '0_main', order: 0 @@ -519,7 +542,7 @@ export class AcceptChanges extends AbstractInlineChatAction { } override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController): Promise { - ctrl.acceptSession(); + ctrl.acceptHunk(); } } @@ -549,6 +572,7 @@ export class CancelSessionAction extends AbstractInlineChatAction { } } + export class CloseAction extends AbstractInlineChatAction { constructor() { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 5057e127ba5..951ddfbbefb 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -101,6 +101,7 @@ export class InlineChatController implements IEditorContribution { private static _storageKey = 'inline-chat-history'; private static _promptHistory: string[] = []; private _historyOffset: number = -1; + private _historyCandidate: string = ''; private _historyUpdate: (prompt: string) => void; private readonly _store = new DisposableStore(); @@ -173,6 +174,7 @@ export class InlineChatController implements IEditorContribution { } InlineChatController._promptHistory.unshift(prompt); this._historyOffset = -1; + this._historyCandidate = ''; this._storageService.store(InlineChatController._storageKey, JSON.stringify(InlineChatController._promptHistory), StorageScope.PROFILE, StorageTarget.USER); }; } @@ -230,6 +232,7 @@ export class InlineChatController implements IEditorContribution { this._editor.setSelection(options.initialSelection); } this._historyOffset = -1; + this._historyCandidate = ''; this._onWillStartSession.fire(); this._currentRun = this._nextState(State.CREATE_SESSION, options); await this._currentRun; @@ -986,12 +989,29 @@ export class InlineChatController implements IEditorContribution { if (len === 0) { return; } - const pos = (len + this._historyOffset + (up ? 1 : -1)) % len; - const entry = InlineChatController._promptHistory[pos]; + + if (this._historyOffset === -1) { + // remember the current value + this._historyCandidate = this._zone.value.widget.value; + } + + const newIdx = this._historyOffset + (up ? 1 : -1); + if (newIdx >= len) { + // reached the end + return; + } + + let entry: string; + if (newIdx < 0) { + entry = this._historyCandidate; + this._historyOffset = -1; + } else { + entry = InlineChatController._promptHistory[newIdx]; + this._historyOffset = newIdx; + } this._zone.value.widget.value = entry; this._zone.value.widget.selectAll(); - this._historyOffset = pos; } viewInChat() { @@ -1042,6 +1062,14 @@ export class InlineChatController implements IEditorContribution { this._messages.fire(Message.ACCEPT_SESSION); } + acceptHunk() { + return this._strategy?.acceptHunk(); + } + + discardHunk() { + return this._strategy?.discardHunk(); + } + async cancelSession() { let result: string | undefined; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 56fe6fced08..12f26c6878d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -4,18 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { disposableWindowInterval } from 'vs/base/browser/dom'; -import { IAction, toAction } from 'vs/base/common/actions'; import { coalesceInPlace, equals, tail } from 'vs/base/common/arrays'; import { AsyncIterableObject, AsyncIterableSource } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; import { Lazy } from 'vs/base/common/lazy'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ThemeIcon, themeColorFromId } from 'vs/base/common/themables'; +import { themeColorFromId } from 'vs/base/common/themables'; import { ICodeEditor, IViewZone, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll'; import { LineSource, RenderOptions, renderLines } from 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; @@ -34,7 +31,6 @@ import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IProgress, Progress } from 'vs/platform/progress/common/progress'; -import { IStorageService } from 'vs/platform/storage/common/storage'; import { SaveReason } from 'vs/workbench/common/editor'; import { countWords, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { InlineChatFileCreatePreviewWidget, InlineChatLivePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget'; @@ -76,6 +72,14 @@ export abstract class EditModeStrategy { abstract cancel(): Promise; + async acceptHunk(): Promise { + this._onDidAccept.fire(); + } + + async discardHunk(): Promise { + this._onDidDiscard.fire(); + } + abstract makeProgressiveChanges(targetWindow: Window, edits: ISingleEditOperation[], timings: ProgressingEditsOptions): Promise; abstract makeChanges(targetWindow: Window, edits: ISingleEditOperation[]): Promise; @@ -199,8 +203,6 @@ export class LivePreviewStrategy extends EditModeStrategy { session: Session, private readonly _editor: ICodeEditor, zone: InlineChatZoneWidget, - @IStorageService storageService: IStorageService, - @IBulkEditService bulkEditService: IBulkEditService, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @IInstantiationService private readonly _instaService: IInstantiationService, ) { @@ -243,6 +245,7 @@ export class LivePreviewStrategy extends EditModeStrategy { const targetAltVersion = textModelNSnapshotAltVersion ?? textModelNAltVersion; await undoModelUntil(modelN, targetAltVersion); } + override async makeChanges(_targetWindow: Window, edits: ISingleEditOperation[]): Promise { const cursorStateComputerAndInlineDiffCollection: ICursorStateComputer = (undoEdits) => { let last: Position | null = null; @@ -516,6 +519,9 @@ export class LiveStrategy extends EditModeStrategy { private _editCount: number = 0; + override acceptHunk: () => Promise = () => super.acceptHunk(); + override discardHunk: () => Promise = () => super.discardHunk(); + constructor( session: Session, protected readonly _editor: ICodeEditor, @@ -668,7 +674,8 @@ export class LiveStrategy extends EditModeStrategy { distance: number; position: Position; - actions: IAction[]; + acceptHunk: () => void; + discardHunk: () => void; toggleDiff?: () => any; }; @@ -695,36 +702,25 @@ export class LiveStrategy extends EditModeStrategy { decorationIds.push(decorationsAccessor.addDecoration(change.modifiedRange, this._decoInsertedTextRange)); } - const actions = [ - toAction({ - id: 'accept', - label: localize('accept', "Accept"), - class: ThemeIcon.asClassName(Codicon.check), - run: () => { - // ACCEPT: stop rendering this as inserted - hunkDisplayData.get(hunk)!.acceptedOrRejected = HunkState.Accepted; - renderHunks(); - } - }), - toAction({ - id: 'discard', - label: localize('discard', "Discard"), - class: ThemeIcon.asClassName(Codicon.discard), - run: () => { - const edits: ISingleEditOperation[] = []; - for (let i = 1; i < decorationIds.length; i++) { - // DISCARD: replace modified range with original value. The modified range is retrieved from a decoration - // which was created above so that typing in the editor keeps discard working. - const modifiedRange = this._session.textModelN.getDecorationRange(decorationIds[i])!; - const originalValue = this._session.textModel0.getValueInRange(hunk.changes[i - 1].originalRange); - edits.push(EditOperation.replace(modifiedRange, originalValue)); - } - this._session.textModelN.pushEditOperations(null, edits, () => null); - hunkDisplayData.get(hunk)!.acceptedOrRejected = HunkState.Rejected; - renderHunks(); - } - }), - ]; + const acceptHunk = () => { + // ACCEPT: stop rendering this as inserted + hunkDisplayData.get(hunk)!.acceptedOrRejected = HunkState.Accepted; + renderHunks(); + }; + + const discardHunk = () => { + const edits: ISingleEditOperation[] = []; + for (let i = 1; i < decorationIds.length; i++) { + // DISCARD: replace modified range with original value. The modified range is retrieved from a decoration + // which was created above so that typing in the editor keeps discard working. + const modifiedRange = this._session.textModelN.getDecorationRange(decorationIds[i])!; + const originalValue = this._session.textModel0.getValueInRange(hunk.changes[i - 1].originalRange); + edits.push(EditOperation.replace(modifiedRange, originalValue)); + } + this._session.textModelN.pushEditOperations(null, edits, () => null); + hunkDisplayData.get(hunk)!.acceptedOrRejected = HunkState.Rejected; + renderHunks(); + }; // original view zone const mightContainNonBasicASCII = this._session.textModel0.mightContainNonBasicASCII() ?? false; @@ -776,8 +772,9 @@ export class LiveStrategy extends EditModeStrategy { viewZone: viewZoneData, distance: myDistance, position: modifiedRange.getStartPosition().delta(-1), + acceptHunk, + discardHunk, toggleDiff: !hunk.original.isEmpty ? toggleDiff : undefined, - actions }; hunkDisplayData.set(hunk, data); @@ -813,7 +810,6 @@ export class LiveStrategy extends EditModeStrategy { }); if (widgetData) { - this._zone.widget.setExtraButtons(widgetData.actions); this._zone.updatePositionAndHeight(widgetData.position); this._editor.revealPositionInCenterIfOutsideViewport(widgetData.position); @@ -822,6 +818,8 @@ export class LiveStrategy extends EditModeStrategy { this._ctxCurrentChangeHasDiff.set(Boolean(widgetData.toggleDiff)); this.toggleDiff = widgetData.toggleDiff; + this.acceptHunk = async () => widgetData!.acceptHunk(); + this.discardHunk = async () => widgetData!.discardHunk(); } else if (hunkDisplayData.size > 0) { // everything accepted or rejected @@ -843,7 +841,6 @@ export class LiveStrategy extends EditModeStrategy { renderHunks(); this._renderStore.add(toDisposable(() => { - this._zone.widget.setExtraButtons([]); changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => { for (const data of hunkDisplayData.values()) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 6678164b6f5..669caac3ae4 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -46,7 +46,7 @@ import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibil import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ExpansionState } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar, WorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; +import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; @@ -67,7 +67,6 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IChatReplyFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; -import { IAction } from 'vs/base/common/actions'; const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); @@ -183,7 +182,6 @@ export class InlineChatWidget { h('div.followUps.hidden@followUps'), h('div.status@status', [ h('div.label.info.hidden@infoLabel'), - h('div.actions.hidden@extraToolbar'), h('div.actions.hidden@statusToolbar'), h('div.label.status.hidden@statusLabel'), h('div.actions.hidden@feedbackToolbar'), @@ -571,15 +569,6 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } - private _extraButtonsCleanup = this._store.add(new MutableDisposable()); - - setExtraButtons(buttons: IAction[]) { - const bar = this._instantiationService.createInstance(WorkbenchButtonBar, this._elements.extraToolbar, { telemetrySource: 'inlineChat' }); - bar.update(buttons); - this._elements.extraToolbar.classList.toggle('hidden', buttons.length === 0); - this._extraButtonsCleanup.value = bar; - } - get expansionState(): ExpansionState { return this._expansionState; } @@ -743,7 +732,6 @@ export class InlineChatWidget { reset(this._elements.statusLabel); this._elements.detectedIntent.classList.toggle('hidden', true); this._elements.statusLabel.classList.toggle('hidden', true); - this._elements.extraToolbar.classList.add('hidden'); this._elements.statusToolbar.classList.add('hidden'); this._elements.feedbackToolbar.classList.add('hidden'); this.updateInfo('');