Joh/homely-damselfly (#213376)

* chore - `ReplyResponse` cleanup

* associate hunk data with response id

* Associate hunk data with response state so that accepting hunks updates the text edit group

* first cut of moving N-edits conversion from inline chat to panel
This commit is contained in:
Johannes Rieken 2024-05-24 11:14:49 +02:00 committed by GitHub
parent 6bd20f68f8
commit 2e98100d22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 180 additions and 129 deletions

View file

@ -621,7 +621,7 @@ export function registerChatCodeCompareBlockActions() {
const instaService = accessor.get(IInstantiationService);
const editor = instaService.createInstance(DefaultChatTextEditor);
await editor.apply(context.element, context.edit);
await editor.apply(context.element, context.edit, context.diffEditor);
await editorService.openEditor({
resource: context.edit.uri,

View file

@ -81,6 +81,7 @@ import { IMarkdownVulnerability, annotateSpecialMarkdownContent } from '../commo
import { CodeBlockModelCollection } from '../common/codeBlockModelCollection';
import { IChatListItemRendererOptions } from './chat';
import { ChatMarkdownRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownRenderer';
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
const $ = dom.$;
@ -1072,12 +1073,34 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
false
);
store.add(modified);
if (!chatTextEdit.state?.applied) {
for (const group of chatTextEdit.edits) {
const edits = group.map(TextEdit.asEditOperation);
modified.pushEditOperations(null, edits, () => null);
const editGroups: ISingleEditOperation[][] = [];
if (isResponseVM(element)) {
const chatModel = this.chatService.getSession(element.sessionId)!;
for (const request of chatModel.getRequests()) {
if (!request.response) {
continue;
}
for (const item of request.response.response.value) {
if (item.kind !== 'textEditGroup' || item.state?.applied) {
continue;
}
for (const group of item.edits) {
const edits = group.map(TextEdit.asEditOperation);
editGroups.push(edits);
}
}
if (request.response === element.model) {
break;
}
}
}
for (const edits of editGroups) {
modified.pushEditOperations(null, edits, () => null);
}
return {
modified,
original,

View file

@ -775,7 +775,7 @@ export class DefaultChatTextEditor {
@IDialogService private readonly dialogService: IDialogService,
) { }
async apply(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup): Promise<void> {
async apply(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup, diffEditor: IDiffEditor | undefined): Promise<void> {
if (!response.response.value.includes(item)) {
// bogous item
@ -787,15 +787,16 @@ export class DefaultChatTextEditor {
return;
}
let diffEditor: IDiffEditor | undefined;
for (const candidate of this.editorService.listDiffEditors()) {
if (!candidate.getContainerDomNode().isConnected) {
continue;
}
const model = candidate.getModel();
if (!model || !isEqual(model.original.uri, item.uri) || model.modified.uri.scheme !== Schemas.vscodeChatCodeCompareBlock) {
diffEditor = candidate;
break;
if (!diffEditor) {
for (const candidate of this.editorService.listDiffEditors()) {
if (!candidate.getContainerDomNode().isConnected) {
continue;
}
const model = candidate.getModel();
if (!model || !isEqual(model.original.uri, item.uri) || model.modified.uri.scheme !== Schemas.vscodeChatCodeCompareBlock) {
diffEditor = candidate;
break;
}
}
}

View file

@ -51,13 +51,15 @@ export interface IChatRequestModel {
readonly response?: IChatResponseModel;
}
export interface IChatTextEditGroupState {
sha1: string;
applied: number;
}
export interface IChatTextEditGroup {
uri: URI;
edits: TextEdit[][];
state?: {
sha1: string;
applied: number;
};
state?: IChatTextEditGroupState;
kind: 'textEditGroup';
}

View file

@ -40,12 +40,14 @@ import { StashedSession } from './inlineChatSession';
import { IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model';
import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget';
import { MessageController } from 'vs/editor/contrib/message/browser/messageController';
import { ChatModel, IChatRequestModel, IResponse } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatModel, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from 'vs/workbench/contrib/chat/common/chatModel';
import { InlineChatError } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart';
import { isEqual } from 'vs/base/common/resources';
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService';
import { generateUuid } from 'vs/base/common/uuid';
import { isEqual } from 'vs/base/common/resources';
export const enum State {
CREATE_SESSION = 'CREATE_SESSION',
@ -653,6 +655,13 @@ export class InlineChatController implements IEditorContribution {
let lastLength = 0;
let isFirstChange = true;
const sha1 = new DefaultModelSHA1Computer();
const textModel0Sha1 = sha1.canComputeSHA1(this._session.textModel0)
? sha1.computeSHA1(this._session.textModel0)
: generateUuid();
const editState: IChatTextEditGroupState = { sha1: textModel0Sha1, applied: 0 };
let localEditGroup: IChatTextEditGroup | undefined;
// apply edits
store.add(response.onDidChange(() => {
@ -667,17 +676,18 @@ export class InlineChatController implements IEditorContribution {
return;
}
const edits = response.response.value.map(part => {
if (part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)) {
return part.edits;
} else {
return [];
}
}).flat();
if (!localEditGroup) {
localEditGroup = <IChatTextEditGroup>response.response.value.find(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri));
}
// const edits = response.edits.get(this._session!.textModelN.uri) ?? [];
if (!localEditGroup) {
return;
}
localEditGroup.state ??= editState;
const edits = localEditGroup.edits;
const newEdits = edits.slice(lastLength);
// console.log('NEW edits', newEdits, edits);
if (newEdits.length === 0) {
return; // NO change
}
@ -720,7 +730,7 @@ export class InlineChatController implements IEditorContribution {
const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced');
this._session.wholeRange.fixup(diff?.changes ?? []);
await this._session.hunkData.recompute();
await this._session.hunkData.recompute(editState);
this._zone.value.widget.updateToolbar(true);
this._zone.value.widget.updateProgress(false);
@ -1007,15 +1017,33 @@ export class InlineChatController implements IEditorContribution {
return;
}
// TODO@jrieken REMOVE this as soon as we can mark responses as accepted
// and as soon as hunks support request-linking
const textEditsResponseCount = this._session.chatModel.getRequests().filter(request => request.response?.response.value.some(part => part.kind === 'textEditGroup')).length;
if (textEditsResponseCount > 1) {
return;
let someApplied = false;
let lastEdit: IChatTextEditGroup | undefined;
const uri = this._editor.getModel()?.uri;
const requests = this._session.chatModel.getRequests();
for (const request of requests) {
if (!request.response) {
continue;
}
for (const part of request.response.response.value) {
if (part.kind === 'textEditGroup' && isEqual(part.uri, uri)) {
// fully or partially applied edits
someApplied = someApplied || Boolean(part.state?.applied);
lastEdit = part;
}
}
}
const doEdits = this._strategy.cancel();
if (someApplied) {
assertType(lastEdit);
lastEdit.edits = [doEdits];
}
this._strategy.cancel();
await this._instaService.invokeFunction(moveToPanelChat, this._session?.chatModel);
this.cancelSession();
}

View file

@ -6,16 +6,15 @@
import { URI } from 'vs/base/common/uri';
import { Emitter, Event } from 'vs/base/common/event';
import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { IWorkspaceTextEdit, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages';
import { TextEdit } from 'vs/editor/common/languages';
import { IIdentifiedSingleEditOperation, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation, TrackedRangeStickiness } from 'vs/editor/common/model';
import { EditMode, IInlineChatSession, IInlineChatBulkEditResponse, IInlineChatEditResponse, InlineChatResponseType, CTX_INLINE_CHAT_HAS_STASHED_SESSION } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { EditMode, IInlineChatSession, CTX_INLINE_CHAT_HAS_STASHED_SESSION, IInlineChatResponse } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { isCancellationError } from 'vs/base/common/errors';
import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import { DetailedLineRangeMapping, LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ILanguageService } from 'vs/editor/common/languages/language';
@ -32,7 +31,7 @@ import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ILogService } from 'vs/platform/log/common/log';
import { ChatModel, IChatRequestModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatModel, IChatRequestModel, IChatResponseModel, IChatTextEditGroupState } from 'vs/workbench/contrib/chat/common/chatModel';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IChatAgent } from 'vs/workbench/contrib/chat/common/chatAgents';
@ -316,56 +315,40 @@ export class ErrorResponse {
export class ReplyResponse {
readonly allLocalEdits: TextEdit[][] = [];
readonly untitledTextModel: IUntitledTextEditorModel | undefined;
readonly workspaceEdit: WorkspaceEdit | undefined;
constructor(
readonly raw: IInlineChatBulkEditResponse | IInlineChatEditResponse,
readonly mdContent: IMarkdownString,
readonly raw: IInlineChatResponse,
localUri: URI,
readonly modelAltVersionId: number,
progressEdits: TextEdit[][],
readonly requestId: string,
readonly chatResponse: IChatResponseModel | undefined,
readonly chatRequest: IChatRequestModel,
readonly chatResponse: IChatResponseModel,
@ITextFileService private readonly _textFileService: ITextFileService,
@ILanguageService private readonly _languageService: ILanguageService,
) {
const editsMap = new ResourceMap<TextEdit[][]>();
const edits = ResourceEdit.convert(raw.edits);
editsMap.set(localUri, [...progressEdits]);
if (raw.type === InlineChatResponseType.EditorEdit) {
//
editsMap.get(localUri)!.push(raw.edits);
} else if (raw.type === InlineChatResponseType.BulkEdit) {
//
const edits = ResourceEdit.convert(raw.edits);
for (const edit of edits) {
if (edit instanceof ResourceFileEdit) {
if (edit.newResource && !edit.oldResource) {
editsMap.set(edit.newResource, []);
if (edit.options.contents) {
console.warn('CONTENT not supported');
}
}
} else if (edit instanceof ResourceTextEdit) {
//
const array = editsMap.get(edit.resource);
if (array) {
array.push([edit.textEdit]);
} else {
editsMap.set(edit.resource, [[edit.textEdit]]);
for (const edit of edits) {
if (edit instanceof ResourceFileEdit) {
if (edit.newResource && !edit.oldResource) {
editsMap.set(edit.newResource, []);
if (edit.options.contents) {
console.warn('CONTENT not supported');
}
}
} else if (edit instanceof ResourceTextEdit) {
//
const array = editsMap.get(edit.resource);
if (array) {
array.push([edit.textEdit]);
} else {
editsMap.set(edit.resource, [[edit.textEdit]]);
}
}
}
let needsWorkspaceEdit = false;
for (const [uri, edits] of editsMap) {
@ -376,8 +359,6 @@ export class ReplyResponse {
}
const isLocalUri = isEqual(uri, localUri);
needsWorkspaceEdit = needsWorkspaceEdit || (uri.scheme !== Schemas.untitled && !isLocalUri);
if (uri.scheme === Schemas.untitled && !isLocalUri && !this.untitledTextModel) { //TODO@jrieken the first untitled model WINS
const langSelection = this._languageService.createByFilepathOrFirstLine(uri, undefined);
const untitledTextModel = this._textFileService.untitled.create({
@ -388,18 +369,6 @@ export class ReplyResponse {
untitledTextModel.resolve();
}
}
this.allLocalEdits = editsMap.get(localUri) ?? [];
if (needsWorkspaceEdit) {
const workspaceEdits: IWorkspaceTextEdit[] = [];
for (const [uri, edits] of editsMap) {
for (const edit of edits.flat()) {
workspaceEdits.push({ resource: uri, textEdit: edit, versionId: undefined });
}
}
this.workspaceEdit = { edits: workspaceEdits };
}
}
}
@ -471,7 +440,7 @@ export class HunkData {
private static readonly _HUNK_THRESHOLD = 8;
private readonly _store = new DisposableStore();
private readonly _data = new Map<RawHunk, { textModelNDecorations: string[]; textModel0Decorations: string[]; state: HunkState }>();
private readonly _data = new Map<RawHunk, RawHunkData>();
private _ignoreChanges: boolean = false;
constructor(
@ -602,7 +571,7 @@ export class HunkData {
this._textModel0.pushEditOperations(null, edits, () => null);
}
async recompute() {
async recompute(editState: IChatTextEditGroupState) {
const diff = await this._editorWorkerService.computeDiff(this._textModel0.uri, this._textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false }, 'advanced');
@ -656,6 +625,7 @@ export class HunkData {
}
this._data.set(hunk, {
editState,
textModelNDecorations,
textModel0Decorations,
state: HunkState.Pending
@ -689,7 +659,7 @@ export class HunkData {
discardAll() {
const edits: ISingleEditOperation[][] = [];
for (const item of this.getInfo()) {
if (item.getState() !== HunkState.Rejected) {
if (item.getState() === HunkState.Pending) {
edits.push(this._discardEdits(item));
}
}
@ -746,6 +716,7 @@ export class HunkData {
}
this._textModel0.pushEditOperations(null, edits, () => null);
data.state = HunkState.Accepted;
data.editState.applied += 1;
}
}
};
@ -764,6 +735,13 @@ class RawHunk {
) { }
}
type RawHunkData = {
textModelNDecorations: string[];
textModel0Decorations: string[];
state: HunkState;
editState: IChatTextEditGroupState;
};
export const enum HunkState {
Pending = 0,
Accepted = 1,

View file

@ -5,7 +5,6 @@
import { CancellationToken } from 'vs/base/common/cancellation';
import { CancellationError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
@ -23,7 +22,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor';
import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { CTX_INLINE_CHAT_HAS_PROVIDER, EditMode, IInlineChatBulkEditResponse, IInlineChatSession, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { CTX_INLINE_CHAT_HAS_PROVIDER, EditMode, IInlineChatResponse, IInlineChatSession } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput';
import { EmptyResponse, ErrorResponse, HunkData, ReplyResponse, Session, SessionExchange, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession';
@ -199,17 +198,11 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
inlineResponse = new EmptyResponse();
} else {
// replay response
const markdownContent = new MarkdownString();
const raw: IInlineChatBulkEditResponse = {
id: Math.random(),
type: InlineChatResponseType.BulkEdit,
message: markdownContent,
const raw: IInlineChatResponse = {
edits: { edits: [] },
};
for (const item of response.response.value) {
if (item.kind === 'markdownContent') {
markdownContent.value += item.content.value;
} else if (item.kind === 'textEditGroup') {
if (item.kind === 'textEditGroup') {
for (const group of item.edits) {
for (const edit of group) {
raw.edits.edits.push({
@ -225,12 +218,10 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
inlineResponse = this._instaService.createInstance(
ReplyResponse,
raw,
markdownContent,
session.textModelN.uri,
modelAltVersionIdNow,
[],
e.request.id,
e.request.response
e.request,
response
);
}

View file

@ -99,7 +99,7 @@ export abstract class EditModeStrategy {
continue;
}
await editor.apply(request.response, item);
await editor.apply(request.response, item, undefined);
if (item.uri.scheme === Schemas.untitled) {
const untitled = this._textFileService.untitled.get(item.uri);

View file

@ -5,7 +5,7 @@
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IRange } from 'vs/editor/common/core/range';
import { TextEdit, WorkspaceEdit } from 'vs/editor/common/languages';
import { WorkspaceEdit } from 'vs/editor/common/languages';
import { localize } from 'vs/nls';
import { MenuId } from 'vs/platform/actions/common/actions';
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
@ -24,13 +24,6 @@ export interface IInlineChatSession {
wholeRange?: IRange;
}
export type IInlineChatResponse = IInlineChatEditResponse | IInlineChatBulkEditResponse;
export const enum InlineChatResponseType {
EditorEdit = 'editorEdit',
BulkEdit = 'bulkEdit'
}
export const enum InlineChatResponseTypes {
Empty = 'empty',
OnlyEdits = 'onlyEdits',
@ -38,18 +31,7 @@ export const enum InlineChatResponseTypes {
Mixed = 'mixed'
}
export interface IInlineChatEditResponse {
id: number;
type: InlineChatResponseType.EditorEdit;
edits: TextEdit[];
message?: IMarkdownString;
placeholder?: string;
wholeRange?: IRange;
}
export interface IInlineChatBulkEditResponse {
id: number;
type: InlineChatResponseType.BulkEdit;
export interface IInlineChatResponse {
edits: WorkspaceEdit;
message?: IMarkdownString;
placeholder?: string;
@ -77,7 +59,6 @@ export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey<boolean>('inli
export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input"));
export const CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey<boolean>('inlineChatHasActiveRequest', false, localize('inlineChatHasActiveRequest', "Whether interactive editor has an active request"));
export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey<boolean>('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore"));
export const CTX_INLINE_CHAT_LAST_RESPONSE_TYPE = new RawContextKey<InlineChatResponseType | undefined>('inlineChatLastResponseType', undefined, localize('inlineChatResponseType', "What type was the last response of the current interactive editor session"));
export const CTX_INLINE_CHAT_RESPONSE_TYPES = new RawContextKey<InlineChatResponseTypes | undefined>('inlineChatResponseTypes', InlineChatResponseTypes.Empty, localize('inlineChatResponseTypes', "What type was the responses have been receieved"));
export const CTX_INLINE_CHAT_DID_EDIT = new RawContextKey<boolean>('inlineChatDidEdit', undefined, localize('inlineChatDidEdit', "Whether interactive editor did change any code"));
export const CTX_INLINE_CHAT_USER_DID_EDIT = new RawContextKey<boolean>('inlineChatUserDidEdit', undefined, localize('inlineChatUserDidEdit', "Whether the user did changes ontop of the inline chat"));

View file

@ -160,7 +160,7 @@ suite('InlineChatSession', function () {
} finally {
session.hunkData.ignoreTextModelNChanges = false;
}
await session.hunkData.recompute();
await session.hunkData.recompute({ applied: 0, sha1: 'fakeSha1' });
}
function makeEdit(edit: EditOperation | EditOperation[]) {
@ -433,4 +433,51 @@ suite('InlineChatSession', function () {
assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue());
});
test('HunkData, accept, discardAll', async function () {
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]);
assert.strictEqual(session.hunkData.size, 2);
assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer()));
const textModeNNow = session.textModelN.getValue();
session.hunkData.getInfo()[0].acceptChanges();
assert.strictEqual(textModeNNow, session.textModelN.getValue());
session.hunkData.discardAll(); // all remaining
assert.strictEqual(session.textModelN.getValue(), 'AI_EDIT\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven');
assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue());
inlineChatSessionService.releaseSession(session);
});
test('HunkData, discardAll return undo edits', async function () {
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]);
assert.strictEqual(session.hunkData.size, 2);
assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer()));
const textModeNNow = session.textModelN.getValue();
session.hunkData.getInfo()[0].acceptChanges();
assert.strictEqual(textModeNNow, session.textModelN.getValue());
const undoEdits = session.hunkData.discardAll(); // all remaining
assert.strictEqual(session.textModelN.getValue(), 'AI_EDIT\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven');
assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue());
// undo the discards
session.textModelN.pushEditOperations(null, undoEdits, () => null);
assert.strictEqual(textModeNNow, session.textModelN.getValue());
inlineChatSessionService.releaseSession(session);
});
});