Skip to content

Commit 4f2ff19

Browse files
authored
Merge pull request microsoft#205164 from microsoft/rebornix/healthy-buzzard
Ensure escape discard/accept changes properly.
2 parents 7f359bb + 76aa317 commit 4f2ff19

File tree

8 files changed

+98
-29
lines changed

8 files changed

+98
-29
lines changed

src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkey
1414
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
1515
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
1616
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';
17-
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';
17+
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';
1818
import { NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController';
1919
import { INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getEditorFromArgsOrActivePane } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
2020
import { CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
2121
import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
22-
import { NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
22+
import { NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
2323

2424

2525
registerAction2(class extends NotebookAction {
@@ -222,11 +222,18 @@ registerAction2(class extends NotebookAction {
222222
shortTitle: localize('apply2', 'Accept'),
223223
icon: Codicon.check,
224224
tooltip: localize('apply3', 'Accept Changes'),
225-
keybinding: {
226-
when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED),
227-
weight: KeybindingWeight.EditorContrib + 10,
228-
primary: KeyMod.CtrlCmd | KeyCode.Enter,
229-
},
225+
keybinding: [
226+
{
227+
when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED),
228+
weight: KeybindingWeight.EditorContrib + 10,
229+
primary: KeyMod.CtrlCmd | KeyCode.Enter,
230+
},
231+
{
232+
when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_USER_DID_EDIT),
233+
weight: KeybindingWeight.EditorCore + 10,
234+
primary: KeyCode.Escape
235+
}
236+
],
230237
menu: [
231238
{
232239
id: MENU_CELL_CHAT_WIDGET_STATUS,
@@ -251,7 +258,7 @@ registerAction2(class extends NotebookAction {
251258
title: localize('discard', 'Discard'),
252259
icon: Codicon.discard,
253260
keybinding: {
254-
when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, NOTEBOOK_CELL_LIST_FOCUSED),
261+
when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_USER_DID_EDIT.negate()),
255262
weight: KeybindingWeight.EditorContrib,
256263
primary: KeyCode.Escape
257264
},

src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
99

1010
export const CTX_NOTEBOOK_CELL_CHAT_FOCUSED = new RawContextKey<boolean>('notebookCellChatFocused', false, localize('notebookCellChatFocused', "Whether the cell chat editor is focused"));
1111
export const CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey<boolean>('notebookChatHasActiveRequest', false, localize('notebookChatHasActiveRequest', "Whether the cell chat editor has an active request"));
12+
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"));
1213
export const MENU_CELL_CHAT_INPUT = MenuId.for('cellChatInput');
1314
export const MENU_CELL_CHAT_WIDGET = MenuId.for('cellChatWidget');
1415
export const MENU_CELL_CHAT_WIDGET_STATUS = MenuId.for('cellChatWidget.status');

src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { Schemas } from 'vs/base/common/network';
1313
import { MovingAverage } from 'vs/base/common/numbers';
1414
import { StopWatch } from 'vs/base/common/stopwatch';
1515
import { assertType } from 'vs/base/common/types';
16-
import { URI } from 'vs/base/common/uri';
1716
import { generateUuid } from 'vs/base/common/uuid';
1817
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
1918
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
@@ -40,11 +39,12 @@ import { IInlineChatMessageAppender, InlineChatWidget } from 'vs/workbench/contr
4039
import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/utils';
4140
import { CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, EditMode, IInlineChatProgressItem, IInlineChatRequest, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
4241
import { insertCell, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations';
43-
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';
42+
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';
4443
import { INotebookEditor, INotebookEditorContribution, INotebookViewZone, ScrollToRevealBehavior } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
4544
import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
4645
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl';
4746
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
47+
import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
4848

4949

5050

@@ -100,10 +100,21 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone {
100100
this.inlineChatWidget.focus();
101101
}
102102

103-
async getEditingCellEditor() {
103+
getEditingCell() {
104+
return this._editingCell;
105+
}
106+
107+
async getOrCreateEditingCell(): Promise<{ cell: CellViewModel; editor: IActiveCodeEditor } | undefined> {
104108
if (this._editingCell) {
105109
await this._notebookEditor.focusNotebookCell(this._editingCell, 'editor');
106-
return this._notebookEditor.activeCodeEditor;
110+
if (this._notebookEditor.activeCodeEditor?.hasModel()) {
111+
return {
112+
cell: this._editingCell,
113+
editor: this._notebookEditor.activeCodeEditor
114+
};
115+
} else {
116+
return undefined;
117+
}
107118
}
108119

109120
if (!this._notebookEditor.hasModel()) {
@@ -117,7 +128,14 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone {
117128
}
118129

119130
await this._notebookEditor.focusNotebookCell(this._editingCell, 'editor', { revealBehavior: ScrollToRevealBehavior.firstLine });
120-
return this._notebookEditor.activeCodeEditor;
131+
if (this._notebookEditor.activeCodeEditor?.hasModel()) {
132+
return {
133+
cell: this._editingCell,
134+
editor: this._notebookEditor.activeCodeEditor
135+
};
136+
}
137+
138+
return undefined;
121139
}
122140

123141
async discardChange() {
@@ -160,6 +178,8 @@ export class NotebookChatController extends Disposable implements INotebookEdito
160178
private _activeSession?: Session;
161179
private readonly _ctxHasActiveRequest: IContextKey<boolean>;
162180
private readonly _ctxCellWidgetFocused: IContextKey<boolean>;
181+
private readonly _ctxUserDidEdit: IContextKey<boolean>;
182+
private readonly _userEditingDisposables = this._register(new DisposableStore());
163183
private readonly _ctxLastResponseType: IContextKey<undefined | InlineChatResponseType>;
164184
private _widget: NotebookChatWidget | undefined;
165185
private _widgetDisposableStore = this._register(new DisposableStore());
@@ -174,12 +194,14 @@ export class NotebookChatController extends Disposable implements INotebookEdito
174194
@IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService,
175195
@IModelService private readonly _modelService: IModelService,
176196
@ILanguageService private readonly _languageService: ILanguageService,
197+
@INotebookExecutionStateService private _executionStateService: INotebookExecutionStateService,
177198

178199
) {
179200
super();
180201
this._ctxHasActiveRequest = CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.bindTo(this._contextKeyService);
181202
this._ctxCellWidgetFocused = CTX_NOTEBOOK_CELL_CHAT_FOCUSED.bindTo(this._contextKeyService);
182203
this._ctxLastResponseType = CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.bindTo(this._contextKeyService);
204+
this._ctxUserDidEdit = CTX_NOTEBOOK_CHAT_USER_DID_EDIT.bindTo(this._contextKeyService);
183205
}
184206

185207
run(index: number, input: string | undefined, autoSend: boolean | undefined): void {
@@ -206,6 +228,10 @@ export class NotebookChatController extends Disposable implements INotebookEdito
206228
}
207229

208230
private _createWidget(index: number, input: string | undefined, autoSend: boolean | undefined) {
231+
if (!this._notebookEditor.hasModel()) {
232+
return;
233+
}
234+
209235
// Clear the widget if it's already there
210236
this._widgetDisposableStore.clear();
211237

@@ -230,8 +256,9 @@ export class NotebookChatController extends Disposable implements INotebookEdito
230256
{ isSimpleWidget: true }
231257
));
232258

233-
const inputBoxPath = `/notebook-chat-input-${NotebookChatController.counter++}`;
234-
const inputUri = URI.from({ scheme: Schemas.untitled, path: inputBoxPath });
259+
const inputBoxFragment = `notebook-chat-input-${NotebookChatController.counter++}`;
260+
const notebookUri = this._notebookEditor.textModel.uri;
261+
const inputUri = notebookUri.with({ scheme: Schemas.untitled, fragment: inputBoxFragment });
235262
const result: ITextModel = this._modelService.createModel('', null, inputUri, false);
236263
fakeParentEditor.setModel(result);
237264

@@ -270,8 +297,9 @@ export class NotebookChatController extends Disposable implements INotebookEdito
270297
this._languageService
271298
);
272299

300+
this._ctxCellWidgetFocused.set(true);
301+
273302
disposableTimeout(() => {
274-
this._ctxCellWidgetFocused.set(true);
275303
this._focusWidget();
276304
}, 0, this._store);
277305

@@ -330,6 +358,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito
330358
}
331359

332360
this._notebookEditor.focusContainer(true);
361+
this._notebookEditor.setFocus({ start: this._widget.afterModelPosition, end: this._widget.afterModelPosition });
333362
this._notebookEditor.setSelections([{
334363
start: this._widget.afterModelPosition,
335364
end: this._widget.afterModelPosition
@@ -476,6 +505,22 @@ export class NotebookChatController extends Disposable implements INotebookEdito
476505
});
477506
}
478507
}
508+
509+
this._userEditingDisposables.clear();
510+
// monitor user edits
511+
const editingCell = this._widget.getEditingCell();
512+
if (editingCell) {
513+
this._userEditingDisposables.add(editingCell.model.onDidChangeContent(() => this._updateUserEditingState()));
514+
this._userEditingDisposables.add(editingCell.model.onDidChangeLanguage(() => this._updateUserEditingState()));
515+
this._userEditingDisposables.add(editingCell.model.onDidChangeMetadata(() => this._updateUserEditingState()));
516+
this._userEditingDisposables.add(editingCell.model.onDidChangeInternalMetadata(() => this._updateUserEditingState()));
517+
this._userEditingDisposables.add(editingCell.model.onDidChangeOutputs(() => this._updateUserEditingState()));
518+
this._userEditingDisposables.add(this._executionStateService.onDidChangeExecution(e => {
519+
if (e.type === NotebookExecutionType.cell && e.affectsCell(editingCell.uri)) {
520+
this._updateUserEditingState();
521+
}
522+
}));
523+
}
479524
}
480525
} catch (e) {
481526
response = new ErrorResponse(e);
@@ -519,12 +564,14 @@ export class NotebookChatController extends Disposable implements INotebookEdito
519564
assertType(this._strategy);
520565
assertType(this._widget);
521566

522-
const editor = await this._widget.getEditingCellEditor();
567+
const editingCell = await this._widget.getOrCreateEditingCell();
523568

524-
if (!editor || !editor.hasModel()) {
569+
if (!editingCell) {
525570
return;
526571
}
527572

573+
const editor = editingCell.editor;
574+
528575
const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(editor.getModel().uri, edits);
529576
// this._log('edits from PROVIDER and after making them MORE MINIMAL', this._activeSession.provider.debugName, edits, moreMinimalEdits);
530577

@@ -551,6 +598,10 @@ export class NotebookChatController extends Disposable implements INotebookEdito
551598
}
552599
}
553600

601+
private _updateUserEditingState() {
602+
this._ctxUserDidEdit.set(true);
603+
}
604+
554605
async acceptSession() {
555606
assertType(this._activeSession);
556607
assertType(this._strategy);
@@ -615,6 +666,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito
615666
discard() {
616667
this._strategy?.cancel();
617668
this._widget?.discardChange();
669+
this.dismiss();
618670
}
619671

620672
async feedbackLast(kind: InlineChatResponseFeedbackKind) {
@@ -627,6 +679,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito
627679

628680
dismiss() {
629681
this._ctxCellWidgetFocused.set(false);
682+
this._ctxUserDidEdit.set(false);
630683
this._sessionCtor?.cancel();
631684
this._sessionCtor = undefined;
632685
this._widget?.dispose();

src/vs/workbench/contrib/notebook/browser/controller/editActions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { IDialogService, IConfirmationResult } from 'vs/platform/dialogs/common/
3232
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
3333
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
3434
import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
35+
import { CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
3536

3637

3738
const CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID = 'notebook.clearAllCellsOutputs';
@@ -85,7 +86,8 @@ registerAction2(class EditCellAction extends NotebookCellAction {
8586

8687
const quitEditCondition = ContextKeyExpr.and(
8788
NOTEBOOK_EDITOR_FOCUSED,
88-
InputFocusedContext
89+
InputFocusedContext,
90+
CTX_INLINE_CHAT_FOCUSED.toNegated()
8991
);
9092
registerAction2(class QuitEditCellAction extends NotebookCellAction {
9193
constructor() {

src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -361,9 +361,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
361361
this._focused = focused;
362362
}
363363

364-
/**
365-
* Empty selection will be turned to `null`
366-
*/
367364
validateRange(cellRange: ICellRange | null | undefined): ICellRange | null {
368365
if (!cellRange) {
369366
return null;
@@ -372,11 +369,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
372369
const start = clamp(cellRange.start, 0, this.length);
373370
const end = clamp(cellRange.end, 0, this.length);
374371

375-
if (start === end) {
376-
return null;
377-
}
378-
379-
if (start < end) {
372+
if (start <= end) {
380373
return { start, end };
381374
} else {
382375
return { start: end, end: start };

src/vs/workbench/contrib/notebook/common/notebookRange.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function reduceCellRanges(ranges: ICellRange[]): ICellRange[] {
6565
return [];
6666
}
6767

68-
return sorted.reduce((prev: ICellRange[], curr) => {
68+
const reduced = sorted.reduce((prev: ICellRange[], curr) => {
6969
const last = prev[prev.length - 1];
7070
if (last.end >= curr.start) {
7171
last.end = Math.max(last.end, curr.end);
@@ -74,6 +74,13 @@ export function reduceCellRanges(ranges: ICellRange[]): ICellRange[] {
7474
}
7575
return prev;
7676
}, [first] as ICellRange[]);
77+
78+
if (reduced.length > 1) {
79+
// remove the (0, 0) range
80+
return reduced.filter(range => !(range.start === range.end && range.start === 0));
81+
}
82+
83+
return reduced;
7784
}
7885

7986
export function cellRangesEqual(a: ICellRange[], b: ICellRange[]) {

src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,12 @@ suite('CellRange', function () {
399399
{ start: 0, end: 4 }
400400
]);
401401
});
402+
403+
test('Reduce ranges 2, empty ranges', function () {
404+
assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 0 }, { start: 0, end: 0 }]), [{ start: 0, end: 0 }]);
405+
assert.deepStrictEqual(reduceCellRanges([{ start: 0, end: 0 }, { start: 1, end: 2 }]), [{ start: 1, end: 2 }]);
406+
assert.deepStrictEqual(reduceCellRanges([{ start: 2, end: 2 }]), [{ start: 2, end: 2 }]);
407+
});
402408
});
403409

404410
suite('NotebookWorkingCopyTypeIdentifier', function () {

src/vs/workbench/contrib/notebook/test/browser/notebookSelection.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ suite('NotebookCellList focus/selection', () => {
278278
(editor, viewModel) => {
279279
assert.deepStrictEqual(viewModel.validateRange(null), null);
280280
assert.deepStrictEqual(viewModel.validateRange(undefined), null);
281-
assert.deepStrictEqual(viewModel.validateRange({ start: 0, end: 0 }), null);
281+
assert.deepStrictEqual(viewModel.validateRange({ start: 0, end: 0 }), { start: 0, end: 0 });
282282
assert.deepStrictEqual(viewModel.validateRange({ start: 0, end: 2 }), { start: 0, end: 2 });
283283
assert.deepStrictEqual(viewModel.validateRange({ start: 0, end: 3 }), { start: 0, end: 2 });
284284
assert.deepStrictEqual(viewModel.validateRange({ start: -1, end: 3 }), { start: 0, end: 2 });

0 commit comments

Comments
 (0)