diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 1dc46e8df41..09ea23b2b3b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -26,6 +26,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { ILogService } from 'vs/platform/log/common/log'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { HunkInformation } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); @@ -277,8 +278,8 @@ export class AcceptChanges extends AbstractInlineChatAction { }); } - override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController): Promise { - ctrl.acceptHunk(); + override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, hunk?: HunkInformation | any): Promise { + ctrl.acceptHunk(hunk); } } @@ -313,8 +314,8 @@ export class DiscardHunkAction extends AbstractInlineChatAction { }); } - async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - return ctrl.discardHunk(); + async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, hunk?: HunkInformation | any): Promise { + return ctrl.discardHunk(hunk); } } @@ -510,8 +511,8 @@ export class ToggleDiffForChange extends AbstractInlineChatAction { }); } - override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController): void { - ctrl.toggleDiff(); + override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, hunkInfo: HunkInformation | any): void { + ctrl.toggleDiff(hunkInfo); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 6fe309b4a54..2a26060e2b1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -39,9 +39,9 @@ import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget'; -import { Session, StashedSession } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { HunkInformation, Session, StashedSession } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { InlineChatError } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; -import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; +import { EditModeStrategy, HunkAction, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_SUPPORT_REPORT_ISSUE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; @@ -872,7 +872,7 @@ export class InlineChatController implements IEditorContribution { } if (this._session && !position && (this._session.hasChangedText || this._session.chatModel.hasRequests)) { - widgetPosition = this._session.wholeRange.value.getStartPosition().delta(-1); + widgetPosition = this._session.wholeRange.trackedInitialRange.getStartPosition().delta(-1); } if (!headless) { @@ -1029,10 +1029,6 @@ export class InlineChatController implements IEditorContribution { return this._ui.value.zone.widget.hasFocus(); } - moveHunk(next: boolean) { - this.focus(); - this._strategy?.move?.(next); - } async viewInChat() { if (!this._strategy || !this._session) { @@ -1070,10 +1066,6 @@ export class InlineChatController implements IEditorContribution { this.cancelSession(); } - toggleDiff() { - this._strategy?.toggleDiff?.(); - } - acceptSession(): void { const response = this._session?.chatModel.getRequests().at(-1)?.response; if (response) { @@ -1092,12 +1084,21 @@ export class InlineChatController implements IEditorContribution { this._messages.fire(Message.ACCEPT_SESSION); } - acceptHunk() { - return this._strategy?.acceptHunk(); + acceptHunk(hunkInfo?: HunkInformation) { + return this._strategy?.performHunkAction(hunkInfo, HunkAction.Accept); } - discardHunk() { - return this._strategy?.discardHunk(); + discardHunk(hunkInfo?: HunkInformation) { + return this._strategy?.performHunkAction(hunkInfo, HunkAction.Discard); + } + + toggleDiff(hunkInfo?: HunkInformation) { + return this._strategy?.performHunkAction(hunkInfo, HunkAction.ToggleDiff); + } + + moveHunk(next: boolean) { + this.focus(); + this._strategy?.performHunkAction(undefined, next ? HunkAction.MoveNext : HunkAction.MovePrev); } async cancelSession() { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 161e54d5339..0cbe031f9ee 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -43,12 +43,21 @@ import { isEqual } from 'vs/base/common/resources'; import { generateUuid } from 'vs/base/common/uuid'; import { MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Iterable } from 'vs/base/common/iterator'; export interface IEditObserver { start(): void; stop(): void; } +export const enum HunkAction { + Accept, + Discard, + MoveNext, + MovePrev, + ToggleDiff +} + export abstract class EditModeStrategy { protected static _decoBlock = ModelDecorationOptions.register({ @@ -65,8 +74,6 @@ export abstract class EditModeStrategy { readonly onDidAccept: Event = this._onDidAccept.event; readonly onDidDiscard: Event = this._onDidDiscard.event; - toggleDiff?: () => any; - constructor( protected readonly _session: Session, protected readonly _editor: ICodeEditor, @@ -79,6 +86,14 @@ export abstract class EditModeStrategy { this._store.dispose(); } + performHunkAction(_hunk: HunkInformation | undefined, action: HunkAction) { + if (action === HunkAction.Accept) { + this._onDidAccept.fire(); + } else if (action === HunkAction.Discard) { + this._onDidDiscard.fire(); + } + } + protected async _doApplyChanges(ignoreLocal: boolean): Promise { const untitledModels: IUntitledTextEditorModel[] = []; @@ -125,13 +140,7 @@ export abstract class EditModeStrategy { return this._session.hunkData.discardAll(); } - async acceptHunk(): Promise { - this._onDidAccept.fire(); - } - async discardHunk(): Promise { - this._onDidDiscard.fire(); - } abstract makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, timings: ProgressingEditsOptions, undoStopBefore: boolean): Promise; @@ -139,8 +148,6 @@ export abstract class EditModeStrategy { abstract renderChanges(): Promise; - move?(next: boolean): void; - abstract hasFocus(): boolean; getWholeRangeDecoration(): IModelDeltaDecoration[] { @@ -253,9 +260,6 @@ export class LiveStrategy extends EditModeStrategy { private readonly _progressiveEditingDecorations: IEditorDecorationsCollection; private _editCount: number = 0; - override acceptHunk: () => Promise = () => super.acceptHunk(); - override discardHunk: () => Promise = () => super.discardHunk(); - constructor( session: Session, editor: ICodeEditor, @@ -364,6 +368,58 @@ export class LiveStrategy extends EditModeStrategy { } } + override performHunkAction(hunk: HunkInformation | undefined, action: HunkAction) { + const displayData = this._findDisplayData(hunk); + if (!displayData) { + return; + } + if (action === HunkAction.Accept) { + displayData.acceptHunk(); + } else if (action === HunkAction.Discard) { + displayData.discardHunk(); + } else if (action === HunkAction.MoveNext) { + displayData.move(true); + } else if (action === HunkAction.MovePrev) { + displayData.move(false); + } else if (action === HunkAction.ToggleDiff) { + displayData.toggleDiff?.(); + } + } + + private _findDisplayData(hunkInfo?: HunkInformation) { + let result: HunkDisplayData | undefined; + if (hunkInfo) { + // use context hunk (from tool/buttonbar) + result = this._hunkDisplayData.get(hunkInfo); + } + + if (!result && this._zone.position) { + // find nearest from zone position + const zoneLine = this._zone.position.lineNumber; + let distance: number = Number.MAX_SAFE_INTEGER; + for (const candidate of this._hunkDisplayData.values()) { + if (candidate.hunk.getState() !== HunkState.Pending) { + continue; + } + const hunkRanges = candidate.hunk.getRangesN(); + const myDistance = zoneLine <= hunkRanges[0].startLineNumber + ? hunkRanges[0].startLineNumber - zoneLine + : zoneLine - hunkRanges[0].endLineNumber; + + if (myDistance < distance) { + distance = myDistance; + result = candidate; + } + } + } + + if (!result) { + // fallback: first hunk that is pending + result = Iterable.first(Iterable.filter(this._hunkDisplayData.values(), candidate => candidate.hunk.getState() === HunkState.Pending)); + } + return result; + } + private readonly _hunkDisplayData = new Map(); override async renderChanges() { @@ -465,29 +521,13 @@ export class LiveStrategy extends EditModeStrategy { }; const move = (next: boolean) => { - assertType(widgetData); - - const candidates: Position[] = []; - for (const item of this._session.hunkData.getInfo()) { - if (item.getState() === HunkState.Pending) { - candidates.push(item.getRangesN()[0].getStartPosition().delta(-1)); - } - } - if (candidates.length < 2) { - return; - } - for (let i = 0; i < candidates.length; i++) { - if (candidates[i].equals(widgetData.position)) { - let newPos: Position; - if (next) { - newPos = candidates[(i + 1) % candidates.length]; - } else { - newPos = candidates[(i + candidates.length - 1) % candidates.length]; - } - this._zone.updatePositionAndHeight(newPos); - renderHunks(); - break; - } + const keys = Array.from(this._hunkDisplayData.keys()); + const idx = keys.indexOf(hunkData); + const nextIdx = (idx + (next ? 1 : -1) + keys.length) % keys.length; + if (nextIdx !== idx) { + const nextData = this._hunkDisplayData.get(keys[nextIdx])!; + this._zone.updatePositionAndHeight(nextData?.position); + renderHunks(); } }; @@ -552,10 +592,6 @@ 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(); - this.move = next => widgetData!.move(next); } else if (this._hunkDisplayData.size > 0) { // everything accepted or rejected @@ -643,6 +679,7 @@ class InlineChangeOverlay implements IOverlayWidget { if (_hunkInfo.getState() === HunkState.Pending) { this._store.add(this._instaService.createInstance(MenuWorkbenchButtonBar, this._domNode, MENU_INLINE_CHAT_ZONE, { + menuOptions: { arg: _hunkInfo }, telemetrySource: 'inlineChat-changesZone', buttonConfigProvider: (_action, idx) => { return { @@ -650,7 +687,7 @@ class InlineChangeOverlay implements IOverlayWidget { showIcon: true, showLabel: false }; - } + }, })); }