Live mode uses chat widget rendering (#210157)

* reveal editor after making changes

* also give preview models a path component

* workaround errors in diff computer fyi @hediet

* * make live mode be a rendering mode
* render new files with the chat widget and not with a separate zone

* 💄

* fix tests
This commit is contained in:
Johannes Rieken 2024-04-11 14:43:53 +02:00 committed by GitHub
parent 0424b0b26a
commit 7b91c2d1ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 84 additions and 87 deletions

View file

@ -67,6 +67,16 @@ export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider, I
return this.diffAlgorithm.computeDiff(original, modified, options, cancellationToken);
}
if (original.isDisposed() || modified.isDisposed()) {
// TODO@hediet
return {
changes: [],
identical: true,
quitEarly: false,
moves: [],
};
}
// This significantly speeds up the case when the original file is empty
if (original.getLineCount() === 1 && original.getLineMaxColumn(1) === 1) {
if (modified.getLineCount() === 1 && modified.getLineMaxColumn(1) === 1) {

View file

@ -616,6 +616,9 @@ export function registerChatCodeCompareBlockActions() {
}
async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise<any> {
const editorService = accessor.get(IEditorService);
const model = context.diffEditor.getModel();
if (!model) {
return;
@ -635,6 +638,12 @@ export function registerChatCodeCompareBlockActions() {
model.original.pushStackElement();
model.original.pushEditOperations(null, edits, () => null);
model.original.pushStackElement();
await editorService.openEditor({
resource: model.original.uri,
options: { revealIfVisible: true },
});
}
});

View file

@ -380,11 +380,7 @@ export class ReplyResponse {
languageId: langSelection.languageId
});
this.untitledTextModel = untitledTextModel;
untitledTextModel.resolve().then(async () => {
const model = untitledTextModel.textEditorModel!;
model.applyEdits(flatEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)));
});
untitledTextModel.resolve();
}
}

View file

@ -506,7 +506,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
} else if (item.kind === 'textEdit') {
for (const edit of item.edits) {
raw.edits.edits.push({
resource: session.textModelN.uri,
resource: item.uri,
textEdit: edit,
versionId: undefined
});
@ -570,19 +570,9 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
const id = generateUuid();
const targetUri = textModel.uri;
let textModelN: ITextModel;
if (options.editMode === EditMode.Preview) {
// AI edits happen in a copy
textModelN = store.add(this._modelService.createModel(
createTextBufferFactoryFromSnapshot(textModel.createSnapshot()),
{ languageId: textModel.getLanguageId(), onDidChange: Event.None },
targetUri.with({ scheme: Schemas.vscode, authority: 'inline-chat', path: '', query: new URLSearchParams({ id, 'textModelN': '' }).toString() })
));
} else {
// AI edits happen in the actual model, keep a reference but make no copy
store.add((await this._textModelService.createModelReference(textModel.uri)));
textModelN = textModel;
}
// AI edits happen in the actual model, keep a reference but make no copy
store.add((await this._textModelService.createModelReference(textModel.uri)));
const textModelN = textModel;
// create: keep a snapshot of the "actual" model
const textModel0 = store.add(this._modelService.createModel(

View file

@ -7,13 +7,12 @@ import { WindowIntervalTimer } from 'vs/base/browser/dom';
import { coalesceInPlace } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { Lazy } from 'vs/base/common/lazy';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { themeColorFromId } from 'vs/base/common/themables';
import { ICodeEditor, IViewZone, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser';
import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll';
import { LineSource, RenderOptions, renderLines } from 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines';
import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
@ -24,11 +23,9 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel';
import { localize } from 'vs/nls';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Progress } from 'vs/platform/progress/common/progress';
import { SaveReason } from 'vs/workbench/common/editor';
import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter';
import { InlineChatFileCreatePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget';
import { HunkInformation, ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { InlineChatZoneWidget } from './inlineChatZoneWidget';
import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
@ -38,6 +35,8 @@ import { IModelService } from 'vs/editor/common/services/model';
import { performAsyncTextEdit, asProgressiveEdit } from './utils';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TextEdit } from 'vs/editor/common/languages';
import { isEqual } from 'vs/base/common/resources';
export interface IEditObserver {
start(): void;
@ -74,7 +73,38 @@ export abstract class EditModeStrategy {
this._store.dispose();
}
abstract apply(): Promise<void>;
async apply(): Promise<void> {
if (this._session.lastExchange?.response instanceof ReplyResponse) {
const { untitledTextModel } = this._session.lastExchange.response;
if (untitledTextModel && !untitledTextModel.isDisposed()) {
await untitledTextModel.resolve();
if (!untitledTextModel.textEditorModel) {
return;
}
// TODO@jrieken
// apply changes only when not dirty. This is very proper and needs to
// fixed in the future
if (!untitledTextModel.isDirty()) {
const allEdits: TextEdit[] = [];
for (const request of this._session.chatModel.getRequests()) {
for (const item of request.response?.response.value ?? []) {
if (item.kind === 'textEdit' && isEqual(item.uri, untitledTextModel.resource)) {
allEdits.push(...item.edits);
}
}
}
untitledTextModel.textEditorModel.pushStackElement();
untitledTextModel.textEditorModel.pushEditOperations(null, allEdits.map(TextEdit.asEditOperation), () => null);
untitledTextModel.textEditorModel.pushStackElement();
}
await untitledTextModel.save({ reason: SaveReason.EXPLICIT });
}
}
}
cancel() {
return this._session.hunkData.discardAll();
@ -140,7 +170,6 @@ export abstract class EditModeStrategy {
export class PreviewStrategy extends EditModeStrategy {
private readonly _ctxDocumentChanged: IContextKey<boolean>;
private readonly _previewZone: Lazy<InlineChatFileCreatePreviewWidget>;
constructor(
session: Session,
@ -148,7 +177,6 @@ export class PreviewStrategy extends EditModeStrategy {
zone: InlineChatZoneWidget,
@IModelService modelService: IModelService,
@IContextKeyService contextKeyService: IContextKeyService,
@IInstantiationService instaService: IInstantiationService,
) {
super(session, editor, zone);
@ -160,50 +188,40 @@ export class PreviewStrategy extends EditModeStrategy {
this._ctxDocumentChanged.set(session.hasChangedText);
}
}, undefined, this._store);
this._previewZone = new Lazy(() => instaService.createInstance(InlineChatFileCreatePreviewWidget, editor));
}
override dispose(): void {
this._ctxDocumentChanged.reset();
this._previewZone.rawValue?.dispose();
super.dispose();
}
async apply() {
override async apply() {
// (1) ensure the editor still shows the original text
// (2) accept all pending hunks (moves changes from N to 0)
// (3) replace editor model with textModel0
// apply all edits from all responses
const textModel = this._editor.getModel();
if (textModel?.equalsTextBuffer(this._session.textModel0.getTextBuffer())) {
this._session.hunkData.getInfo().forEach(item => item.acceptChanges());
const newText = this._session.textModel0.getValue();
const range = textModel.getFullModelRange();
textModel.pushStackElement();
textModel.pushEditOperations(null, [EditOperation.replace(range, newText)], () => null);
textModel.pushStackElement();
}
if (this._session.lastExchange?.response instanceof ReplyResponse) {
const { untitledTextModel } = this._session.lastExchange.response;
if (untitledTextModel && !untitledTextModel.isDisposed() && untitledTextModel.isDirty()) {
await untitledTextModel.save({ reason: SaveReason.EXPLICIT });
const allEdits: TextEdit[] = [];
for (const request of this._session.chatModel.getRequests()) {
for (const item of request.response?.response.value ?? []) {
if (item.kind === 'textEdit' && isEqual(item.uri, textModel.uri)) {
allEdits.push(...item.edits);
}
}
}
textModel.pushStackElement();
textModel.pushEditOperations(null, allEdits.map(TextEdit.asEditOperation), () => null);
textModel.pushStackElement();
}
await super.apply();
}
override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver): Promise<void> {
return this._makeChanges(edits, obs, undefined, undefined);
}
override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions): Promise<void> {
await this._makeChanges(edits, obs, opts, new Progress<any>(() => {
this._zone.widget.showEditsPreview(this._session.hunkData, this._session.textModel0, this._session.textModelN);
}));
}
override async undoChanges(altVersionId: number): Promise<void> {
@ -212,17 +230,7 @@ export class PreviewStrategy extends EditModeStrategy {
}
override async renderChanges(response: ReplyResponse): Promise<undefined> {
if (response.allLocalEdits.length > 0) {
this._zone.widget.showEditsPreview(this._session.hunkData, this._session.textModel0, this._session.textModelN);
} else {
this._zone.widget.hideEditsPreview();
}
if (response.untitledTextModel && !response.untitledTextModel.isDisposed()) {
this._previewZone.value.showCreation(this._session.wholeRange.value.getStartPosition().delta(-1), response.untitledTextModel);
} else {
this._previewZone.rawValue?.hide();
}
}
hasFocus(): boolean {
@ -279,8 +287,6 @@ export class LiveStrategy extends EditModeStrategy {
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
});
private readonly _previewZone: Lazy<InlineChatFileCreatePreviewWidget>;
private readonly _ctxCurrentChangeHasDiff: IContextKey<boolean>;
private readonly _ctxCurrentChangeShowsDiff: IContextKey<boolean>;
@ -297,20 +303,17 @@ export class LiveStrategy extends EditModeStrategy {
@IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
@IConfigurationService private readonly _configService: IConfigurationService,
@IInstantiationService protected readonly _instaService: IInstantiationService,
) {
super(session, editor, zone);
this._ctxCurrentChangeHasDiff = CTX_INLINE_CHAT_CHANGE_HAS_DIFF.bindTo(contextKeyService);
this._ctxCurrentChangeShowsDiff = CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF.bindTo(contextKeyService);
this._progressiveEditingDecorations = this._editor.createDecorationsCollection();
this._previewZone = new Lazy(() => _instaService.createInstance(InlineChatFileCreatePreviewWidget, editor));
}
override dispose(): void {
this._resetDiff();
this._previewZone.rawValue?.dispose();
super.dispose();
}
@ -326,18 +329,12 @@ export class LiveStrategy extends EditModeStrategy {
}
}
async apply() {
override async apply() {
this._resetDiff();
if (this._editCount > 0) {
this._editor.pushUndoStop();
}
if (!(this._session.lastExchange?.response instanceof ReplyResponse)) {
return;
}
const { untitledTextModel } = this._session.lastExchange.response;
if (untitledTextModel && !untitledTextModel.isDisposed() && untitledTextModel.isDirty()) {
await untitledTextModel.save({ reason: SaveReason.EXPLICIT });
}
await super.apply();
}
override cancel() {
@ -381,12 +378,6 @@ export class LiveStrategy extends EditModeStrategy {
override async renderChanges(response: ReplyResponse) {
if (response.untitledTextModel && !response.untitledTextModel.isDisposed()) {
this._previewZone.value.showCreation(this._session.wholeRange.value.getStartPosition().delta(-1), response.untitledTextModel);
} else {
this._previewZone.rawValue?.hide();
}
this._progressiveEditingDecorations.clear();
const renderHunks = () => {

View file

@ -14,12 +14,13 @@ import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
import { localize } from 'vs/nls';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, EditMode, InlineChatConfigKeys, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { EditorBasedInlineChatWidget } from './inlineChatWidget';
import { MenuId } from 'vs/platform/actions/common/actions';
import { isEqual } from 'vs/base/common/resources';
import { StableEditorBottomScrollState } from 'vs/editor/browser/stableEditorScroll';
import { ScrollType } from 'vs/editor/common/editorCommon';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export class InlineChatZoneWidget extends ZoneWidget {
@ -33,7 +34,8 @@ export class InlineChatZoneWidget extends ZoneWidget {
constructor(
editor: ICodeEditor,
@IInstantiationService private readonly _instaService: IInstantiationService,
@IContextKeyService contextKeyService: IContextKeyService
@IContextKeyService contextKeyService: IContextKeyService,
@IConfigurationService configurationService: IConfigurationService,
) {
super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 });
@ -63,9 +65,10 @@ export class InlineChatZoneWidget extends ZoneWidget {
},
rendererOptions: {
renderTextEditsAsSummary: (uri) => {
// render edits as summary only when using Live mode and when
// dealing with the current file in the editor
return isEqual(uri, editor.getModel()?.uri)
// && !"true"
;
&& configurationService.getValue<EditMode>(InlineChatConfigKeys.Mode) === EditMode.Live;
},
}
});

View file

@ -8,7 +8,6 @@ import { equals } from 'vs/base/common/arrays';
import { timeout } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { mock } from 'vs/base/test/common/mock';
import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
@ -561,7 +560,6 @@ suite('InteractiveChatController', function () {
assert.strictEqual(requests.length, 2);
assert.strictEqual(requests[0].previewDocument.toString(), model.uri.toString()); // live
assert.strictEqual(requests[1].previewDocument.scheme, Schemas.vscode); // preview
assert.strictEqual(requests[1].previewDocument.authority, 'inline-chat');
assert.strictEqual(requests[1].previewDocument.toString(), model.uri.toString()); // preview (both use the same but edits aren't applied like that)
});
});