Merge pull request #205164 from microsoft/rebornix/healthy-buzzard

Ensure escape discard/accept changes properly.
This commit is contained in:
Peng Lyu
2024-02-13 18:28:27 -08:00
committed by GitHub
8 changed files with 98 additions and 29 deletions
@@ -14,12 +14,12 @@ import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkey
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext';
import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext';
import { NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController';
import { INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getEditorFromArgsOrActivePane } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
import { CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
import { NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
registerAction2(class extends NotebookAction {
@@ -222,11 +222,18 @@ registerAction2(class extends NotebookAction {
shortTitle: localize('apply2', 'Accept'),
icon: Codicon.check,
tooltip: localize('apply3', 'Accept Changes'),
keybinding: {
when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED),
weight: KeybindingWeight.EditorContrib + 10,
primary: KeyMod.CtrlCmd | KeyCode.Enter,
},
keybinding: [
{
when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED),
weight: KeybindingWeight.EditorContrib + 10,
primary: KeyMod.CtrlCmd | KeyCode.Enter,
},
{
when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_USER_DID_EDIT),
weight: KeybindingWeight.EditorCore + 10,
primary: KeyCode.Escape
}
],
menu: [
{
id: MENU_CELL_CHAT_WIDGET_STATUS,
@@ -251,7 +258,7 @@ registerAction2(class extends NotebookAction {
title: localize('discard', 'Discard'),
icon: Codicon.discard,
keybinding: {
when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, NOTEBOOK_CELL_LIST_FOCUSED),
when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_USER_DID_EDIT.negate()),
weight: KeybindingWeight.EditorContrib,
primary: KeyCode.Escape
},
@@ -9,6 +9,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
export const CTX_NOTEBOOK_CELL_CHAT_FOCUSED = new RawContextKey<boolean>('notebookCellChatFocused', false, localize('notebookCellChatFocused', "Whether the cell chat editor is focused"));
export const CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey<boolean>('notebookChatHasActiveRequest', false, localize('notebookChatHasActiveRequest', "Whether the cell chat editor has an active request"));
export const CTX_NOTEBOOK_CHAT_USER_DID_EDIT = new RawContextKey<boolean>('notebookChatUserDidEdit', false, localize('notebookChatUserDidEdit', "Whether the user did changes ontop of the notebook cell chat"));
export const MENU_CELL_CHAT_INPUT = MenuId.for('cellChatInput');
export const MENU_CELL_CHAT_WIDGET = MenuId.for('cellChatWidget');
export const MENU_CELL_CHAT_WIDGET_STATUS = MenuId.for('cellChatWidget.status');
@@ -13,7 +13,6 @@ import { Schemas } from 'vs/base/common/network';
import { MovingAverage } from 'vs/base/common/numbers';
import { StopWatch } from 'vs/base/common/stopwatch';
import { assertType } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
@@ -40,11 +39,12 @@ import { IInlineChatMessageAppender, InlineChatWidget } from 'vs/workbench/contr
import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/utils';
import { CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, EditMode, IInlineChatProgressItem, IInlineChatRequest, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { insertCell, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations';
import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext';
import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext';
import { INotebookEditor, INotebookEditorContribution, INotebookViewZone, ScrollToRevealBehavior } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl';
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
@@ -100,10 +100,21 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone {
this.inlineChatWidget.focus();
}
async getEditingCellEditor() {
getEditingCell() {
return this._editingCell;
}
async getOrCreateEditingCell(): Promise<{ cell: CellViewModel; editor: IActiveCodeEditor } | undefined> {
if (this._editingCell) {
await this._notebookEditor.focusNotebookCell(this._editingCell, 'editor');
return this._notebookEditor.activeCodeEditor;
if (this._notebookEditor.activeCodeEditor?.hasModel()) {
return {
cell: this._editingCell,
editor: this._notebookEditor.activeCodeEditor
};
} else {
return undefined;
}
}
if (!this._notebookEditor.hasModel()) {
@@ -117,7 +128,14 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone {
}
await this._notebookEditor.focusNotebookCell(this._editingCell, 'editor', { revealBehavior: ScrollToRevealBehavior.firstLine });
return this._notebookEditor.activeCodeEditor;
if (this._notebookEditor.activeCodeEditor?.hasModel()) {
return {
cell: this._editingCell,
editor: this._notebookEditor.activeCodeEditor
};
}
return undefined;
}
async discardChange() {
@@ -160,6 +178,8 @@ export class NotebookChatController extends Disposable implements INotebookEdito
private _activeSession?: Session;
private readonly _ctxHasActiveRequest: IContextKey<boolean>;
private readonly _ctxCellWidgetFocused: IContextKey<boolean>;
private readonly _ctxUserDidEdit: IContextKey<boolean>;
private readonly _userEditingDisposables = this._register(new DisposableStore());
private readonly _ctxLastResponseType: IContextKey<undefined | InlineChatResponseType>;
private _widget: NotebookChatWidget | undefined;
private _widgetDisposableStore = this._register(new DisposableStore());
@@ -174,12 +194,14 @@ export class NotebookChatController extends Disposable implements INotebookEdito
@IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService,
@IModelService private readonly _modelService: IModelService,
@ILanguageService private readonly _languageService: ILanguageService,
@INotebookExecutionStateService private _executionStateService: INotebookExecutionStateService,
) {
super();
this._ctxHasActiveRequest = CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.bindTo(this._contextKeyService);
this._ctxCellWidgetFocused = CTX_NOTEBOOK_CELL_CHAT_FOCUSED.bindTo(this._contextKeyService);
this._ctxLastResponseType = CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.bindTo(this._contextKeyService);
this._ctxUserDidEdit = CTX_NOTEBOOK_CHAT_USER_DID_EDIT.bindTo(this._contextKeyService);
}
run(index: number, input: string | undefined, autoSend: boolean | undefined): void {
@@ -206,6 +228,10 @@ export class NotebookChatController extends Disposable implements INotebookEdito
}
private _createWidget(index: number, input: string | undefined, autoSend: boolean | undefined) {
if (!this._notebookEditor.hasModel()) {
return;
}
// Clear the widget if it's already there
this._widgetDisposableStore.clear();
@@ -230,8 +256,9 @@ export class NotebookChatController extends Disposable implements INotebookEdito
{ isSimpleWidget: true }
));
const inputBoxPath = `/notebook-chat-input-${NotebookChatController.counter++}`;
const inputUri = URI.from({ scheme: Schemas.untitled, path: inputBoxPath });
const inputBoxFragment = `notebook-chat-input-${NotebookChatController.counter++}`;
const notebookUri = this._notebookEditor.textModel.uri;
const inputUri = notebookUri.with({ scheme: Schemas.untitled, fragment: inputBoxFragment });
const result: ITextModel = this._modelService.createModel('', null, inputUri, false);
fakeParentEditor.setModel(result);
@@ -270,8 +297,9 @@ export class NotebookChatController extends Disposable implements INotebookEdito
this._languageService
);
this._ctxCellWidgetFocused.set(true);
disposableTimeout(() => {
this._ctxCellWidgetFocused.set(true);
this._focusWidget();
}, 0, this._store);
@@ -330,6 +358,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito
}
this._notebookEditor.focusContainer(true);
this._notebookEditor.setFocus({ start: this._widget.afterModelPosition, end: this._widget.afterModelPosition });
this._notebookEditor.setSelections([{
start: this._widget.afterModelPosition,
end: this._widget.afterModelPosition
@@ -476,6 +505,22 @@ export class NotebookChatController extends Disposable implements INotebookEdito
});
}
}
this._userEditingDisposables.clear();
// monitor user edits
const editingCell = this._widget.getEditingCell();
if (editingCell) {
this._userEditingDisposables.add(editingCell.model.onDidChangeContent(() => this._updateUserEditingState()));
this._userEditingDisposables.add(editingCell.model.onDidChangeLanguage(() => this._updateUserEditingState()));
this._userEditingDisposables.add(editingCell.model.onDidChangeMetadata(() => this._updateUserEditingState()));
this._userEditingDisposables.add(editingCell.model.onDidChangeInternalMetadata(() => this._updateUserEditingState()));
this._userEditingDisposables.add(editingCell.model.onDidChangeOutputs(() => this._updateUserEditingState()));
this._userEditingDisposables.add(this._executionStateService.onDidChangeExecution(e => {
if (e.type === NotebookExecutionType.cell && e.affectsCell(editingCell.uri)) {
this._updateUserEditingState();
}
}));
}
}
} catch (e) {
response = new ErrorResponse(e);
@@ -519,12 +564,14 @@ export class NotebookChatController extends Disposable implements INotebookEdito
assertType(this._strategy);
assertType(this._widget);
const editor = await this._widget.getEditingCellEditor();
const editingCell = await this._widget.getOrCreateEditingCell();
if (!editor || !editor.hasModel()) {
if (!editingCell) {
return;
}
const editor = editingCell.editor;
const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(editor.getModel().uri, edits);
// this._log('edits from PROVIDER and after making them MORE MINIMAL', this._activeSession.provider.debugName, edits, moreMinimalEdits);
@@ -551,6 +598,10 @@ export class NotebookChatController extends Disposable implements INotebookEdito
}
}
private _updateUserEditingState() {
this._ctxUserDidEdit.set(true);
}
async acceptSession() {
assertType(this._activeSession);
assertType(this._strategy);
@@ -615,6 +666,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito
discard() {
this._strategy?.cancel();
this._widget?.discardChange();
this.dismiss();
}
async feedbackLast(kind: InlineChatResponseFeedbackKind) {
@@ -627,6 +679,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito
dismiss() {
this._ctxCellWidgetFocused.set(false);
this._ctxUserDidEdit.set(false);
this._sessionCtor?.cancel();
this._sessionCtor = undefined;
this._widget?.dispose();
@@ -32,6 +32,7 @@ import { IDialogService, IConfirmationResult } from 'vs/platform/dialogs/common/
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
import { CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
const CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID = 'notebook.clearAllCellsOutputs';
@@ -85,7 +86,8 @@ registerAction2(class EditCellAction extends NotebookCellAction {
const quitEditCondition = ContextKeyExpr.and(
NOTEBOOK_EDITOR_FOCUSED,
InputFocusedContext
InputFocusedContext,
CTX_INLINE_CHAT_FOCUSED.toNegated()
);
registerAction2(class QuitEditCellAction extends NotebookCellAction {
constructor() {
@@ -361,9 +361,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
this._focused = focused;
}
/**
* Empty selection will be turned to `null`
*/
validateRange(cellRange: ICellRange | null | undefined): ICellRange | null {
if (!cellRange) {
return null;
@@ -372,11 +369,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
const start = clamp(cellRange.start, 0, this.length);
const end = clamp(cellRange.end, 0, this.length);
if (start === end) {
return null;
}
if (start < end) {
if (start <= end) {
return { start, end };
} else {
return { start: end, end: start };
@@ -65,7 +65,7 @@ export function reduceCellRanges(ranges: ICellRange[]): ICellRange[] {
return [];
}
return sorted.reduce((prev: ICellRange[], curr) => {
const reduced = sorted.reduce((prev: ICellRange[], curr) => {
const last = prev[prev.length - 1];
if (last.end >= curr.start) {
last.end = Math.max(last.end, curr.end);
@@ -74,6 +74,13 @@ export function reduceCellRanges(ranges: ICellRange[]): ICellRange[] {
}
return prev;
}, [first] as ICellRange[]);
if (reduced.length > 1) {
// remove the (0, 0) range
return reduced.filter(range => !(range.start === range.end && range.start === 0));
}
return reduced;
}
export function cellRangesEqual(a: ICellRange[], b: ICellRange[]) {
@@ -399,6 +399,12 @@ suite('CellRange', function () {
{ start: 0, end: 4 }
]);
});
test('Reduce ranges 2, empty ranges', function () {
assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 0 }, { start: 0, end: 0 }]), [{ start: 0, end: 0 }]);
assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 0 }, { start: 1, end: 2 }]), [{ start: 1, end: 2 }]);
assert.deepStrictEqual(reduceCellRanges([{ start: 2, end: 2 }]), [{ start: 2, end: 2 }]);
});
});
suite('NotebookWorkingCopyTypeIdentifier', function () {
@@ -278,7 +278,7 @@ suite('NotebookCellList focus/selection', () => {
(editor, viewModel) => {
assert.deepStrictEqual(viewModel.validateRange(null), null);
assert.deepStrictEqual(viewModel.validateRange(undefined), null);
assert.deepStrictEqual(viewModel.validateRange({ start: 0, end: 0 }), null);
assert.deepStrictEqual(viewModel.validateRange({ start: 0, end: 0 }), { start: 0, end: 0 });
assert.deepStrictEqual(viewModel.validateRange({ start: 0, end: 2 }), { start: 0, end: 2 });
assert.deepStrictEqual(viewModel.validateRange({ start: 0, end: 3 }), { start: 0, end: 2 });
assert.deepStrictEqual(viewModel.validateRange({ start: -1, end: 3 }), { start: 0, end: 2 });