From 9e5ddd42fdfe4f831e056d5cde9e65ebd1e5accd Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 15 May 2023 16:52:49 +0200 Subject: [PATCH] joh/remarkable egret (#182507) * For preview modes bring back actions to discard but keep changes in clipboard or new file, in live modes ESC accepts, in preview is cancels, DropDownWithDefault tweaks so that the first option can always be the default * handle reentrant editor chat with cancel or accept (depending on mode) --- .../browser/menuEntryActionViewItem.ts | 21 +-- .../browser/interactiveEditor.contribution.ts | 7 +- .../browser/interactiveEditorActions.ts | 127 ++++++++---------- .../browser/interactiveEditorController.ts | 70 +++++----- .../browser/interactiveEditorSession.ts | 19 +++ .../browser/interactiveEditorStrategies.ts | 17 ++- .../browser/interactiveEditorWidget.ts | 12 +- .../common/interactiveEditor.ts | 11 +- 8 files changed, 164 insertions(+), 120 deletions(-) diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 87d2e62cffc44..c7faba5bad0dc 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -320,14 +320,15 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { export interface IDropdownWithDefaultActionViewItemOptions extends IDropdownMenuActionViewItemOptions { renderKeybindingWithDefaultActionLabel?: boolean; + persistLastActionId?: boolean; } export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { private readonly _options: IDropdownWithDefaultActionViewItemOptions | undefined; private _defaultAction: ActionViewItem; - private _dropdown: DropdownMenuActionViewItem; + private readonly _dropdown: DropdownMenuActionViewItem; private _container: HTMLElement | null = null; - private _storageKey: string; + private readonly _storageKey: string; get onDidChangeDropdownVisibility(): Event { return this._dropdown.onDidChangeVisibility; @@ -349,7 +350,7 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { // determine default action let defaultAction: IAction | undefined; - const defaultActionId = _storageService.get(this._storageKey, StorageScope.WORKSPACE); + const defaultActionId = options?.persistLastActionId ? _storageService.get(this._storageKey, StorageScope.WORKSPACE) : undefined; if (defaultActionId) { defaultAction = submenuAction.actions.find(a => defaultActionId === a.id); } @@ -359,11 +360,13 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { this._defaultAction = this._instaService.createInstance(MenuEntryActionViewItem, defaultAction, { keybinding: this._getDefaultActionKeybindingLabel(defaultAction) }); - const dropdownOptions = Object.assign({}, options ?? Object.create(null), { + const dropdownOptions: IDropdownMenuActionViewItemOptions = { + keybindingProvider: action => this._keybindingService.lookupKeybinding(action.id), + ...options, menuAsChild: options?.menuAsChild ?? true, classNames: options?.classNames ?? ['codicon', 'codicon-chevron-down'], - actionRunner: options?.actionRunner ?? new ActionRunner() - }); + actionRunner: options?.actionRunner ?? new ActionRunner(), + }; this._dropdown = new DropdownMenuActionViewItem(submenuAction, submenuAction.actions, this._contextMenuService, dropdownOptions); this._dropdown.actionRunner.onDidRun((e: IRunEvent) => { @@ -374,7 +377,9 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { } private update(lastAction: MenuItemAction): void { - this._storageService.store(this._storageKey, lastAction.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); + if (this._options?.persistLastActionId) { + this._storageService.store(this._storageKey, lastAction.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); + } this._defaultAction.dispose(); this._defaultAction = this._instaService.createInstance(MenuEntryActionViewItem, lastAction, { keybinding: this._getDefaultActionKeybindingLabel(lastAction) }); @@ -505,7 +510,7 @@ export function createActionViewItem(instaService: IInstantiationService, action return instaService.createInstance(SubmenuEntrySelectActionViewItem, action); } else { if (action.item.rememberDefaultAction) { - return instaService.createInstance(DropdownWithDefaultActionViewItem, action, options); + return instaService.createInstance(DropdownWithDefaultActionViewItem, action, { ...options, persistLastActionId: true }); } else { return instaService.createInstance(SubmenuEntryActionViewItem, action, options); } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.contribution.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.contribution.ts index 582a8c53d3925..4a48ba8d0fb6a 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.contribution.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.contribution.ts @@ -20,7 +20,11 @@ registerEditorContribution(INTERACTIVE_EDITOR_ID, InteractiveEditorController, E registerAction2(interactiveEditorActions.StartSessionAction); registerAction2(interactiveEditorActions.MakeRequestAction); registerAction2(interactiveEditorActions.StopRequestAction); +registerAction2(interactiveEditorActions.DicardAction); +registerAction2(interactiveEditorActions.DiscardToClipboardAction); +registerAction2(interactiveEditorActions.DiscardUndoToNewFileAction); registerAction2(interactiveEditorActions.CancelSessionAction); + registerAction2(interactiveEditorActions.ArrowOutUpAction); registerAction2(interactiveEditorActions.ArrowOutDownAction); registerAction2(interactiveEditorActions.FocusInteractiveEditor); @@ -30,9 +34,6 @@ registerAction2(interactiveEditorActions.ViewInChatAction); registerAction2(interactiveEditorActions.ExpandMessageAction); registerAction2(interactiveEditorActions.ContractMessageAction); -registerAction2(interactiveEditorActions.UndoToClipboard); -registerAction2(interactiveEditorActions.UndoToNewFile); -registerAction2(interactiveEditorActions.UndoCommand); registerAction2(interactiveEditorActions.ToggleInlineDiff); registerAction2(interactiveEditorActions.FeebackHelpfulCommand); registerAction2(interactiveEditorActions.FeebackUnhelpfulCommand); diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts index 003dddcafc0ff..862877b41df67 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts @@ -10,7 +10,7 @@ import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InteractiveEditorController, InteractiveEditorRunOptions } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController'; -import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_HAS_PROVIDER, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE, MENU_INTERACTIVE_EDITOR_WIDGET_UNDO, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK, CTX_INTERACTIVE_EDITOR_INLNE_DIFF, CTX_INTERACTIVE_EDITOR_EDIT_MODE, EditMode, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE, CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; +import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_HAS_PROVIDER, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK, CTX_INTERACTIVE_EDITOR_INLNE_DIFF, CTX_INTERACTIVE_EDITOR_EDIT_MODE, EditMode, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE, CTX_INTERACTIVE_EDITOR_MESSAGE_CROP_STATE, CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED, CTX_INTERACTIVE_EDITOR_DID_EDIT } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; import { localize } from 'vs/nls'; import { IAction2Options } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -20,7 +20,6 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; -import { ILogService } from 'vs/platform/log/common/log'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Range } from 'vs/editor/common/core/range'; import { fromNow } from 'vs/base/common/date'; @@ -251,86 +250,79 @@ export class NextFromHistory extends AbstractInteractiveEditorAction { } } - -export class UndoToClipboard extends AbstractInteractiveEditorAction { +export class DicardAction extends AbstractInteractiveEditorAction { constructor() { super({ - id: 'interactiveEditor.undoToClipboard', - title: localize('undo.clipboard', 'Undo to Clipboard'), - precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE.isEqualTo('simple')), - keybinding: { - weight: KeybindingWeight.EditorContrib + 10, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyZ }, - }, + id: 'interactiveEditor.discard', + title: localize('discard', 'Discard'), + icon: Codicon.discard, + precondition: CTX_INTERACTIVE_EDITOR_VISIBLE, + // keybinding: { + // weight: KeybindingWeight.EditorContrib - 1, + // primary: KeyCode.Escape + // }, menu: { - when: CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE.isEqualTo('simple'), - id: MENU_INTERACTIVE_EDITOR_WIDGET_UNDO, - group: '1_undo', - order: 1 + id: MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD, + order: 0 } }); } - override runInteractiveEditorCommand(accessor: ServicesAccessor, ctrl: InteractiveEditorController): void { - const clipboardService = accessor.get(IClipboardService); - const lastText = ctrl.undoLast(); - if (lastText !== undefined) { - clipboardService.writeText(lastText); - } + async runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): Promise { + await ctrl.cancelSession(); } } -export class UndoToNewFile extends AbstractInteractiveEditorAction { +export class DiscardToClipboardAction extends AbstractInteractiveEditorAction { constructor() { super({ - id: 'interactiveEditor.undoToFile', - title: localize('undo.newfile', 'Undo to New File'), - precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE.isEqualTo('simple')), + id: 'interactiveEditor.discardToClipboard', + title: localize('undo.clipboard', 'Discard to Clipboard'), + precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_DID_EDIT), + // keybinding: { + // weight: KeybindingWeight.EditorContrib + 10, + // primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ, + // mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyZ }, + // }, menu: { - when: CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE.isEqualTo('simple'), - id: MENU_INTERACTIVE_EDITOR_WIDGET_UNDO, - group: '1_undo', - order: 2 + id: MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD, + order: 1 } }); } - override runInteractiveEditorCommand(accessor: ServicesAccessor, ctrl: InteractiveEditorController, editor: ICodeEditor, ..._args: any[]): void { - const editorService = accessor.get(IEditorService); - const lastText = ctrl.undoLast(); - if (lastText !== undefined) { - const input: IUntitledTextResourceEditorInput = { forceUntitled: true, resource: undefined, contents: lastText, languageId: editor.getModel()?.getLanguageId() }; - editorService.openEditor(input, SIDE_GROUP); + override async runInteractiveEditorCommand(accessor: ServicesAccessor, ctrl: InteractiveEditorController): Promise { + const clipboardService = accessor.get(IClipboardService); + const changedText = await ctrl.cancelSession(); + if (changedText !== undefined) { + clipboardService.writeText(changedText); } } } -export class UndoCommand extends AbstractInteractiveEditorAction { +export class DiscardUndoToNewFileAction extends AbstractInteractiveEditorAction { constructor() { super({ - id: 'interactiveEditor.undo', - title: localize('undo', 'Undo'), - icon: Codicon.commentDiscussion, - precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE.isEqualTo('simple')), - // keybinding: { - // weight: KeybindingWeight.EditorContrib + 10, - // primary: KeyMod.CtrlCmd | KeyCode.KeyZ, - // }, + id: 'interactiveEditor.discardToFile', + title: localize('undo.newfile', 'Discard to New File'), + precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_DID_EDIT), menu: { - when: CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE.isEqualTo('simple'), - id: MENU_INTERACTIVE_EDITOR_WIDGET_UNDO, - group: '1_undo', - order: 3 + id: MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD, + order: 2 } }); } - override runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController): void { - ctrl.undoLast(); + override async runInteractiveEditorCommand(accessor: ServicesAccessor, ctrl: InteractiveEditorController, editor: ICodeEditor, ..._args: any[]): Promise { + const editorService = accessor.get(IEditorService); + const changedText = await ctrl.cancelSession(); + if (changedText !== undefined) { + const input: IUntitledTextResourceEditorInput = { forceUntitled: true, resource: undefined, contents: changedText, languageId: editor.getModel()?.getLanguageId() }; + editorService.openEditor(input, SIDE_GROUP); + } } } @@ -409,30 +401,25 @@ export class ApplyPreviewEdits extends AbstractInteractiveEditorAction { title: localize('applyEdits', 'Apply Changes'), icon: Codicon.check, precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, ContextKeyExpr.or(CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED.toNegated(), CTX_INTERACTIVE_EDITOR_EDIT_MODE.notEqualsTo(EditMode.Preview))), - keybinding: { + keybinding: [{ weight: KeybindingWeight.EditorContrib + 10, - primary: KeyMod.CtrlCmd | KeyCode.Enter - }, + primary: KeyMod.CtrlCmd | KeyCode.Enter, + }, { + weight: KeybindingWeight.EditorContrib + 10, + primary: KeyCode.Escape, + when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.notEqualsTo(EditMode.Preview) + }], menu: { id: MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, + when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.isEqualTo(EditMode.Preview), group: '0_main', order: 0 } }); } - override async runInteractiveEditorCommand(accessor: ServicesAccessor, ctrl: InteractiveEditorController): Promise { - const logService = accessor.get(ILogService); - const editorService = accessor.get(IEditorService); - const edit = await ctrl.applyChanges(); - if (!edit) { - logService.warn('FAILED to apply changes, no edit response'); - return; - } - if (edit.singleCreateFileEdit) { - editorService.openEditor({ resource: edit.singleCreateFileEdit.uri }, SIDE_GROUP); - } - + override async runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController): Promise { + await ctrl.applyChanges(); } } @@ -441,15 +428,16 @@ export class CancelSessionAction extends AbstractInteractiveEditorAction { constructor() { super({ id: 'interactiveEditor.cancel', - title: localize('discard', 'Discard Changes'), + title: localize('cancel', 'Cancel'), icon: Codicon.clearAll, - precondition: CTX_INTERACTIVE_EDITOR_VISIBLE, + precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_EDIT_MODE.isEqualTo(EditMode.Preview)), keybinding: { weight: KeybindingWeight.EditorContrib - 1, primary: KeyCode.Escape }, menu: { id: MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, + when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.isEqualTo(EditMode.Preview), group: '0_main', order: 1 } @@ -468,7 +456,8 @@ export class CopyRecordings extends AbstractInteractiveEditorAction { id: 'interactiveEditor.copyRecordings', f1: true, title: { - value: localize('copyRecordings', '(Developer) Write Exchange to Clipboard'), original: '(Developer) Write Exchange to Clipboard' + value: localize('copyRecordings', '(Developer) Write Exchange to Clipboard'), + original: '(Developer) Write Exchange to Clipboard' } }); } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts index abee1f16e2d02..151feec3b5176 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts @@ -32,7 +32,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { EditResponse, EmptyResponse, ErrorResponse, IInteractiveEditorSessionService, MarkdownResponse, Session, SessionExchange } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorSession'; import { EditModeStrategy, LivePreviewStrategy, LiveStrategy, PreviewStrategy } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorStrategies'; import { InteractiveEditorZoneWidget } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget'; -import { CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE as CTX_INTERACTIVE_EDITOR_LAST_EDIT_KIND, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK as CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK_KIND, IInteractiveEditorRequest, IInteractiveEditorResponse, INTERACTIVE_EDITOR_ID, EditMode, InteractiveEditorResponseFeedbackKind, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, InteractiveEditorResponseType } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; +import { CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK, IInteractiveEditorRequest, IInteractiveEditorResponse, INTERACTIVE_EDITOR_ID, EditMode, InteractiveEditorResponseFeedbackKind, CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE, InteractiveEditorResponseType, CTX_INTERACTIVE_EDITOR_DID_EDIT } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; import { IInteractiveSessionWidgetService } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSession'; import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; @@ -85,7 +85,7 @@ export class InteractiveEditorController implements IEditorContribution { private readonly _zone: InteractiveEditorZoneWidget; private readonly _ctxHasActiveRequest: IContextKey; private readonly _ctxLastResponseType: IContextKey; - private readonly _ctxLastEditKind: IContextKey<'' | 'simple'>; + private readonly _ctxDidEdit: IContextKey; private readonly _ctxLastFeedbackKind: IContextKey<'helpful' | 'unhelpful' | ''>; private _strategy?: EditModeStrategy; @@ -109,9 +109,9 @@ export class InteractiveEditorController implements IEditorContribution { @IAccessibilityService private readonly _accessibilityService: IAccessibilityService ) { this._ctxHasActiveRequest = CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST.bindTo(contextKeyService); - this._ctxLastEditKind = CTX_INTERACTIVE_EDITOR_LAST_EDIT_KIND.bindTo(contextKeyService); + this._ctxDidEdit = CTX_INTERACTIVE_EDITOR_DID_EDIT.bindTo(contextKeyService); this._ctxLastResponseType = CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE.bindTo(contextKeyService); - this._ctxLastFeedbackKind = CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK_KIND.bindTo(contextKeyService); + this._ctxLastFeedbackKind = CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK.bindTo(contextKeyService); this._zone = this._store.add(_instaService.createInstance(InteractiveEditorZoneWidget, this._editor)); this._store.add(this._editor.onDidChangeModel(async e => { @@ -156,10 +156,24 @@ export class InteractiveEditorController implements IEditorContribution { async run(options: InteractiveEditorRunOptions | undefined): Promise { this._logService.trace('[IE] session starting'); + await this._finishOrCancel(); + await this._nextState(State.CREATE_SESSION, { ...options }); this._logService.trace('[IE] session done or paused'); } + private async _finishOrCancel(): Promise { + if (this._activeSession) { + if (this._activeSession.editMode === EditMode.Preview) { + this._logService.trace('[IE] an EXISTING session is active, cancelling first'); + await this.cancelSession(); + } else { + this._logService.trace('[IE] an EXISTING session is active, finishing first'); + await this.applyChanges(); + } + } + } + // ---- state machine private async _nextState(state: State, options: InteractiveEditorRunOptions | undefined): Promise { @@ -171,6 +185,7 @@ export class InteractiveEditorController implements IEditorContribution { } private async [State.CREATE_SESSION](options: InteractiveEditorRunOptions | undefined): Promise { + assertType(this._activeSession === undefined); assertType(this._editor.hasModel()); let session: Session | undefined = options?.existingSession; @@ -277,8 +292,7 @@ export class InteractiveEditorController implements IEditorContribution { // cancel all sibling sessions for (const editor of editors) { if (editor !== this._editor) { - InteractiveEditorController.get(editor)?.cancelSession(); - + InteractiveEditorController.get(editor)?._finishOrCancel(); } } break; @@ -444,6 +458,7 @@ export class InteractiveEditorController implements IEditorContribution { try { this._ignoreModelContentChanged = true; await this._strategy.makeChanges(response, editOperations); + this._ctxDidEdit.set(this._activeSession.hasChangedText); } finally { this._ignoreModelContentChanged = false; } @@ -504,7 +519,7 @@ export class InteractiveEditorController implements IEditorContribution { private async [State.PAUSE]() { assertType(this._activeSession); - this._ctxLastEditKind.reset(); + this._ctxDidEdit.reset(); this._ctxLastResponseType.reset(); this._ctxLastFeedbackKind.reset(); @@ -577,13 +592,6 @@ export class InteractiveEditorController implements IEditorContribution { this._zone.widget.updateToggleState(expand); } - undoLast(): string | void { - if (this._activeSession?.lastExchange?.response instanceof EditResponse) { - this._activeSession.textModelN.undo(); - return this._activeSession.lastExchange.response.localEdits[0].text; - } - } - feedbackLast(helpful: boolean) { if (this._activeSession?.lastExchange?.response instanceof EditResponse || this._activeSession?.lastExchange?.response instanceof MarkdownResponse) { const kind = helpful ? InteractiveEditorResponseFeedbackKind.Helpful : InteractiveEditorResponseFeedbackKind.Unhelpful; @@ -599,7 +607,7 @@ export class InteractiveEditorController implements IEditorContribution { } } - async applyChanges(): Promise { + async applyChanges(): Promise { if (this._strategy) { const strategy = this._strategy; this._strategy = undefined; @@ -612,27 +620,27 @@ export class InteractiveEditorController implements IEditorContribution { } strategy?.dispose(); this._messages.fire(Message.END_SESSION); - - if (this._activeSession?.lastExchange?.response instanceof EditResponse) { - return this._activeSession.lastExchange.response; - } } } async cancelSession() { - if (this._strategy) { - const strategy = this._strategy; - this._strategy = undefined; - try { - await strategy?.cancel(); - } catch (err) { - this._dialogService.error(localize('err.discard', "Failed to discard changes.", toErrorMessage(err))); - this._logService.error('[IE] FAILED to discard changes'); - this._logService.error(err); - } - strategy?.dispose(); - this._messages.fire(Message.END_SESSION); + if (!this._strategy || !this._activeSession) { + return undefined; + } + + const changedText = this._activeSession.asChangedText(); + const strategy = this._strategy; + this._strategy = undefined; + try { + await strategy?.cancel(); + } catch (err) { + this._dialogService.error(localize('err.discard', "Failed to discard changes.", toErrorMessage(err))); + this._logService.error('[IE] FAILED to discard changes'); + this._logService.error(err); } + strategy?.dispose(); + this._messages.fire(Message.END_SESSION); + return changedText; } } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorSession.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorSession.ts index 5eed760f14286..a058908781183 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorSession.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorSession.ts @@ -120,6 +120,25 @@ export class Session { this._lastTextModelChanges = changes; } + get hasChangedText(): boolean { + return !this.textModel0.equalsTextBuffer(this.textModelN.getTextBuffer()); + } + + asChangedText(): string | undefined { + if (!this._lastTextModelChanges || this._lastTextModelChanges.length === 0) { + return undefined; + } + + let startLine = Number.MAX_VALUE; + let endLine = Number.MIN_VALUE; + for (const change of this._lastTextModelChanges) { + startLine = Math.min(startLine, change.modifiedRange.startLineNumber); + endLine = Math.max(endLine, change.modifiedRange.endLineNumberExclusive); + } + + return this.textModelN.getValueInRange(new Range(startLine, 1, endLine, Number.MAX_VALUE)); + } + recordExternalEditOccurred() { this._teldata.edits = true; } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorStrategies.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorStrategies.ts index ab55727035d13..74a498d70e29c 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorStrategies.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorStrategies.ts @@ -18,13 +18,14 @@ import { ICursorStateComputer, IModelDecorationOptions, IModelDeltaDecoration, I import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { InteractiveEditorFileCreatePreviewWidget, InteractiveEditorLivePreviewWidget } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorLivePreviewWidget'; import { EditResponse, Session } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorSession'; import { InteractiveEditorWidget } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget'; import { getValueFromSnapshot } from 'vs/workbench/contrib/interactiveEditor/browser/utils'; import { CTX_INTERACTIVE_EDITOR_INLNE_DIFF, CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; export abstract class EditModeStrategy { @@ -53,6 +54,7 @@ export class PreviewStrategy extends EditModeStrategy { private readonly _widget: InteractiveEditorWidget, @IContextKeyService contextKeyService: IContextKeyService, @IBulkEditService private readonly _bulkEditService: IBulkEditService, + @IInstantiationService private readonly _instaService: IInstantiationService, ) { super(); @@ -85,6 +87,8 @@ export class PreviewStrategy extends EditModeStrategy { const editResponse = this._session.lastExchange?.response; if (editResponse.workspaceEdits) { await this._bulkEditService.apply(editResponse.workspaceEdits); + this._instaService.invokeFunction(showSingleCreateFile, editResponse); + } else if (!editResponse.workspaceEditsIncludeLocalEdits) { @@ -210,6 +214,7 @@ export class LiveStrategy extends EditModeStrategy { @IStorageService protected _storageService: IStorageService, @IBulkEditService protected readonly _bulkEditService: IBulkEditService, @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService, + @IInstantiationService private readonly _instaService: IInstantiationService, ) { super(); this._inlineDiffDecorations = new InlineDiffDecorations(this._editor, this._inlineDiffEnabled); @@ -255,6 +260,7 @@ export class LiveStrategy extends EditModeStrategy { } if (this._lastResponse?.workspaceEdits) { await this._bulkEditService.apply(this._lastResponse.workspaceEdits); + this._instaService.invokeFunction(showSingleCreateFile, this._lastResponse); } } @@ -338,7 +344,7 @@ export class LivePreviewStrategy extends LiveStrategy { @IEditorWorkerService editorWorkerService: IEditorWorkerService, @IInstantiationService instaService: IInstantiationService, ) { - super(session, editor, widget, contextKeyService, storageService, bulkEditService, editorWorkerService); + super(session, editor, widget, contextKeyService, storageService, bulkEditService, editorWorkerService, instaService); this._diffZone = instaService.createInstance(InteractiveEditorLivePreviewWidget, editor, session.textModel0); this._previewZone = instaService.createInstance(InteractiveEditorFileCreatePreviewWidget, editor); @@ -368,3 +374,10 @@ export class LivePreviewStrategy extends LiveStrategy { } } } + +function showSingleCreateFile(accessor: ServicesAccessor, edit: EditResponse) { + const editorService = accessor.get(IEditorService); + if (edit.singleCreateFileEdit) { + editorService.openEditor({ resource: edit.singleCreateFileEdit.uri }, SIDE_GROUP); + } +} diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts index 1ee0e0601d6b6..d2e6ee518f692 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts @@ -28,7 +28,7 @@ import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; -import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { DropdownWithDefaultActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult, TextEdit } from 'vs/editor/common/languages'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; @@ -43,6 +43,7 @@ import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; import { invertLineRange, lineRangeAsRange } from 'vs/workbench/contrib/interactiveEditor/browser/utils'; import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; import { LineRange } from 'vs/editor/common/core/lineRange'; +import { SubmenuItemAction } from 'vs/platform/actions/common/actions'; const _inputEditorOptions: IEditorConstructionOptions = { padding: { top: 3, bottom: 2 }, @@ -264,7 +265,14 @@ export class InteractiveEditorWidget { primaryGroup: () => true, useSeparatorsInPrimaryActions: true }, - actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => createActionViewItem(this._instantiationService, action, options) + actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { + + if (action instanceof SubmenuItemAction) { + return this._instantiationService.createInstance(DropdownWithDefaultActionViewItem, action, { ...options, renderKeybindingWithDefaultActionLabel: true, persistLastActionId: false }); + } + + return createActionViewItem(this._instantiationService, action, options); + } }; const statusToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.statusToolbar, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, workbenchToolbarOptions); this._store.add(statusToolbar); diff --git a/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts b/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts index 453b8f6745e99..711b38dbd18ba 100644 --- a/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactiveEditor/common/interactiveEditor.ts @@ -112,7 +112,7 @@ export const CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION = new RawContextKey<'a export const CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST = new RawContextKey('interactiveEditorHasActiveRequest', false, localize('interactiveEditorHasActiveRequest', "Whether interactive editor has an active request")); export const CTX_INTERACTIVE_EDITOR_INLNE_DIFF = new RawContextKey('interactiveEditorInlineDiff', false, localize('interactiveEditorInlineDiff', "Whether interactive editor show inline diffs for changes")); export const CTX_INTERACTIVE_EDITOR_LAST_RESPONSE_TYPE = new RawContextKey('interactiveEditorLastResponseType', undefined, localize('interactiveEditorResponseType', "What type was the last response of the current interactive editor session")); -export const CTX_INTERACTIVE_EDITOR_LAST_EDIT_TYPE = new RawContextKey<'simple' | ''>('interactiveEditorLastEditKind', '', localize('interactiveEditorLastEditKind', "The last kind of edit that was performed")); +export const CTX_INTERACTIVE_EDITOR_DID_EDIT = new RawContextKey('interactiveEditorDidEdit', false, localize('interactiveEditorDidEdit', "Whether interactive editor did change any code")); export const CTX_INTERACTIVE_EDITOR_LAST_FEEDBACK = new RawContextKey<'unhelpful' | 'helpful' | ''>('interactiveEditorLastFeedbackKind', '', localize('interactiveEditorLastFeedbackKind', "The last kind of feedback that was provided")); export const CTX_INTERACTIVE_EDITOR_DOCUMENT_CHANGED = new RawContextKey('interactiveEditorDocumentChanged', false, localize('interactiveEditorDocumentChanged', "Whether the document has changed concurrently")); export const CTX_INTERACTIVE_EDITOR_EDIT_MODE = new RawContextKey('config.interactiveEditor.editMode', EditMode.Live); @@ -122,14 +122,15 @@ export const CTX_INTERACTIVE_EDITOR_EDIT_MODE = new RawContextKey('con export const MENU_INTERACTIVE_EDITOR_WIDGET = MenuId.for('interactiveEditorWidget'); export const MENU_INTERACTIVE_EDITOR_WIDGET_MARKDOWN_MESSAGE = MenuId.for('interactiveEditorWidget.markdownMessage'); export const MENU_INTERACTIVE_EDITOR_WIDGET_STATUS = MenuId.for('interactiveEditorWidget.status'); -export const MENU_INTERACTIVE_EDITOR_WIDGET_UNDO = MenuId.for('interactiveEditorWidget.undo'); +export const MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD = MenuId.for('interactiveEditorWidget.undo'); MenuRegistry.appendMenuItem(MENU_INTERACTIVE_EDITOR_WIDGET_STATUS, { - submenu: MENU_INTERACTIVE_EDITOR_WIDGET_UNDO, - title: localize('undo', "Undo..."), + submenu: MENU_INTERACTIVE_EDITOR_WIDGET_DISCARD, + title: localize('discard', "Discard..."), icon: Codicon.discard, group: '0_main', order: 2, - when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.isEqualTo('direct') + when: CTX_INTERACTIVE_EDITOR_EDIT_MODE.notEqualsTo(EditMode.Preview), + rememberDefaultAction: true }); // --- colors