Skip to content

Commit e3f315d

Browse files
authored
Computing full diff across a turn (microsoft#258184)
* finding full diff accross a turn * adding code * adding code * using the uri at the end * adding height styling * adding * adding code * investigating * wip * wip * cleaning * creating model reference also * adding code into the chatEditingTimeline * computing the diff * adding history * moving first and last snapshot uris out of the derived * using derivedOpts for snapshot uris * adding code
1 parent 3aef8be commit e3f315d

File tree

4 files changed

+119
-42
lines changed

4 files changed

+119
-42
lines changed

src/vs/workbench/contrib/chat/browser/chatContentParts/chatChangesSummaryPart.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ChatTreeItem } from '../chat.js';
1212
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
1313
import { IChatChangesSummary as IChatFileChangesSummary, IChatService } from '../../common/chatService.js';
1414
import { IEditorService } from '../../../../services/editor/common/editorService.js';
15-
import { IEditSessionEntryDiff } from '../../common/chatEditingService.js';
15+
import { IChatEditingSession, IEditSessionEntryDiff } from '../../common/chatEditingService.js';
1616
import { WorkbenchList } from '../../../../../platform/list/browser/listService.js';
1717
import { ButtonWithIcon } from '../../../../../base/browser/ui/button/button.js';
1818
import { Codicon } from '../../../../../base/common/codicons.js';
@@ -24,7 +24,7 @@ import { IListRenderer, IListVirtualDelegate } from '../../../../../base/browser
2424
import { FileKind } from '../../../../../platform/files/common/files.js';
2525
import { createFileIconThemableTreeContainerScope } from '../../../files/browser/views/explorerView.js';
2626
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
27-
import { autorun, derived, IObservableWithChange } from '../../../../../base/common/observable.js';
27+
import { autorun, derived, IObservable, IObservableWithChange } from '../../../../../base/common/observable.js';
2828
import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js';
2929
import { MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js';
3030
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
@@ -40,6 +40,8 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl
4040
private readonly _onDidChangeHeight = this._register(new Emitter<void>());
4141
public readonly onDidChangeHeight = this._onDidChangeHeight.event;
4242

43+
private readonly diffsBetweenRequests = new Map<string, IObservable<IEditSessionEntryDiff | undefined>>();
44+
4345
private fileChanges: readonly IChatFileChangesSummary[];
4446
private fileChangesDiffsObservable: IObservableWithChange<Map<string, IEditSessionEntryDiff>, void>;
4547

@@ -74,6 +76,8 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl
7476
private computeFileChangesDiffs(context: IChatContentPartRenderContext, changes: readonly IChatFileChangesSummary[]): IObservableWithChange<Map<string, IEditSessionEntryDiff>, void> {
7577
return derived((r) => {
7678
const fileChangesDiffs = new Map<string, IEditSessionEntryDiff>();
79+
const firstRequestId = changes[0].requestId;
80+
const lastRequestId = changes[changes.length - 1].requestId;
7781
for (const change of changes) {
7882
const sessionId = change.sessionId;
7983
const session = this.chatService.getSession(sessionId);
@@ -84,28 +88,26 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl
8488
if (!editSession) {
8589
continue;
8690
}
87-
const uri = change.reference;
88-
const modifiedEntry = editSession.getEntry(uri);
89-
if (!modifiedEntry) {
91+
const diff = this.getCachedEntryDiffBetweenRequests(editSession, change.reference, firstRequestId, lastRequestId)?.read(r);
92+
if (!diff) {
9093
continue;
9194
}
92-
const requestId = change.requestId;
93-
const undoStops = context.content.filter(e => e.kind === 'undoStop');
94-
95-
for (let i = undoStops.length - 1; i >= 0; i--) {
96-
const modifiedUri = modifiedEntry.modifiedURI;
97-
const undoStopID = undoStops[i].id;
98-
const diff = editSession.getEntryDiffBetweenStops(modifiedUri, requestId, undoStopID)?.read(r);
99-
if (!diff) {
100-
continue;
101-
}
102-
fileChangesDiffs.set(this.changeID(change), diff);
103-
}
95+
fileChangesDiffs.set(this.changeID(change), diff);
10496
}
10597
return fileChangesDiffs;
10698
});
10799
}
108100

101+
public getCachedEntryDiffBetweenRequests(editSession: IChatEditingSession, uri: URI, startRequestId: string, stopRequestId: string): IObservable<IEditSessionEntryDiff | undefined> | undefined {
102+
const key = `${uri}\0${startRequestId}\0${stopRequestId}`;
103+
let observable = this.diffsBetweenRequests.get(key);
104+
if (!observable) {
105+
observable = editSession.getEntryDiffBetweenRequests(uri, startRequestId, stopRequestId);
106+
this.diffsBetweenRequests.set(key, observable);
107+
}
108+
return observable;
109+
}
110+
109111
private renderHeader(container: HTMLElement): IDisposable {
110112
const viewListButtonContainer = container.appendChild($('.chat-file-changes-label'));
111113
const viewListButton = new ButtonWithIcon(viewListButtonContainer, {});
@@ -169,11 +171,13 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl
169171
private renderFilesList(container: HTMLElement): IDisposable {
170172
const store = new DisposableStore();
171173
this.list = store.add(this.instantiationService.createInstance(CollapsibleChangesSummaryListPool)).get();
174+
const listNode = this.list.getHTMLElement();
172175
const itemsShown = Math.min(this.fileChanges.length, this.MAX_ITEMS_SHOWN);
173176
const height = itemsShown * this.ELEMENT_HEIGHT;
174177
this.list.layout(height);
178+
listNode.style.height = height + 'px';
175179
this.updateList(this.fileChanges, this.fileChangesDiffsObservable.get());
176-
container.appendChild(this.list.getHTMLElement().parentElement!);
180+
container.appendChild(listNode.parentElement!);
177181

178182
store.add(this.list.onDidOpen((item) => {
179183
const element = item.element;

src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
214214
return this._timeline.getEntryDiffBetweenStops(uri, requestId, stopId);
215215
}
216216

217+
public getEntryDiffBetweenRequests(uri: URI, startRequestId: string, stopRequestId: string) {
218+
return this._timeline.getEntryDiffBetweenRequests(uri, startRequestId, stopRequestId);
219+
}
220+
217221
public createSnapshot(requestId: string, undoStop: string | undefined, makeEmpty = undoStop !== undefined): void {
218222
this._timeline.pushSnapshot(
219223
requestId,

src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTimeline.ts

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77

88
import { equals as arraysEqual, binarySearch2 } from '../../../../../base/common/arrays.js';
9-
import { equals as objectsEqual } from '../../../../../base/common/objects.js';
109
import { findLast } from '../../../../../base/common/arraysFind.js';
1110
import { Iterable } from '../../../../../base/common/iterator.js';
1211
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
1312
import { ResourceMap } from '../../../../../base/common/map.js';
13+
import { equals as objectsEqual } from '../../../../../base/common/objects.js';
1414
import { derived, derivedOpts, IObservable, ITransaction, ObservablePromise, observableValue, transaction } from '../../../../../base/common/observable.js';
1515
import { isEqual } from '../../../../../base/common/resources.js';
1616
import { URI } from '../../../../../base/common/uri.js';
@@ -338,29 +338,7 @@ export class ChatEditingTimeline {
338338

339339
}
340340
const ignoreTrimWhitespace = this._ignoreTrimWhitespaceObservable.read(reader);
341-
const promise = this._editorWorkerService.computeDiff(
342-
refs[0].object.textEditorModel.uri,
343-
refs[1].object.textEditorModel.uri,
344-
{ ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 },
345-
'advanced'
346-
).then((diff): IEditSessionEntryDiff => {
347-
const entryDiff: IEditSessionEntryDiff = {
348-
originalURI: refs[0].object.textEditorModel.uri,
349-
modifiedURI: refs[1].object.textEditorModel.uri,
350-
identical: !!diff?.identical,
351-
quitEarly: !diff || diff.quitEarly,
352-
added: 0,
353-
removed: 0,
354-
};
355-
if (diff) {
356-
for (const change of diff.changes) {
357-
entryDiff.removed += change.original.endLineNumberExclusive - change.original.startLineNumber;
358-
entryDiff.added += change.modified.endLineNumberExclusive - change.modified.startLineNumber;
359-
}
360-
}
361-
362-
return entryDiff;
363-
});
341+
const promise = this._computeDiff(refs[0].object.textEditorModel.uri, refs[1].object.textEditorModel.uri, ignoreTrimWhitespace);
364342

365343
return new ObservablePromise(promise);
366344
});
@@ -418,6 +396,91 @@ export class ChatEditingTimeline {
418396
return observable;
419397
}
420398
}
399+
400+
public getEntryDiffBetweenRequests(uri: URI, startRequestId: string, stopRequestId: string): IObservable<IEditSessionEntryDiff | undefined> {
401+
const snapshotUris = derivedOpts<[URI | undefined, URI | undefined]>(
402+
{ equalsFn: (a, b) => arraysEqual(a, b, isEqual) },
403+
reader => {
404+
const history = this._linearHistory.read(reader);
405+
const firstSnapshotUri = this._getFirstSnapshotForUriAfterRequest(history, uri, startRequestId, true);
406+
const lastSnapshotUri = this._getFirstSnapshotForUriAfterRequest(history, uri, stopRequestId, false);
407+
return [firstSnapshotUri, lastSnapshotUri];
408+
},
409+
);
410+
const modelRefs = derived((reader) => {
411+
const snapshots = snapshotUris.read(reader);
412+
const firstSnapshotUri = snapshots[0];
413+
const lastSnapshotUri = snapshots[1];
414+
if (!firstSnapshotUri || !lastSnapshotUri) {
415+
return;
416+
}
417+
const store = new DisposableStore();
418+
reader.store.add(store);
419+
const referencesPromise = Promise.all([firstSnapshotUri, lastSnapshotUri].map(u => this._textModelService.createModelReference(u))).then(refs => {
420+
if (store.isDisposed) {
421+
refs.forEach(ref => ref.dispose());
422+
} else {
423+
refs.forEach(ref => store.add(ref));
424+
}
425+
return refs;
426+
});
427+
return new ObservablePromise(referencesPromise);
428+
});
429+
const diff = derived((reader): ObservablePromise<IEditSessionEntryDiff> | undefined => {
430+
const references = modelRefs.read(reader)?.promiseResult.read(reader);
431+
const refs = references?.data;
432+
if (!refs) {
433+
return;
434+
}
435+
const ignoreTrimWhitespace = this._ignoreTrimWhitespaceObservable.read(reader);
436+
const promise = this._computeDiff(refs[0].object.textEditorModel.uri, refs[1].object.textEditorModel.uri, ignoreTrimWhitespace);
437+
return new ObservablePromise(promise);
438+
});
439+
return derived(reader => {
440+
return diff.read(reader)?.promiseResult.read(reader)?.data || undefined;
441+
});
442+
}
443+
444+
private _computeDiff(originalUri: URI, modifiedUri: URI, ignoreTrimWhitespace: boolean): Promise<IEditSessionEntryDiff> {
445+
return this._editorWorkerService.computeDiff(
446+
originalUri,
447+
modifiedUri,
448+
{ ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 },
449+
'advanced'
450+
).then((diff): IEditSessionEntryDiff => {
451+
const entryDiff: IEditSessionEntryDiff = {
452+
originalURI: originalUri,
453+
modifiedURI: modifiedUri,
454+
identical: !!diff?.identical,
455+
quitEarly: !diff || diff.quitEarly,
456+
added: 0,
457+
removed: 0,
458+
};
459+
if (diff) {
460+
for (const change of diff.changes) {
461+
entryDiff.removed += change.original.endLineNumberExclusive - change.original.startLineNumber;
462+
entryDiff.added += change.modified.endLineNumberExclusive - change.modified.startLineNumber;
463+
}
464+
}
465+
return entryDiff;
466+
});
467+
}
468+
469+
private _getFirstSnapshotForUriAfterRequest(history: readonly IChatEditingSessionSnapshot[], uri: URI, requestId: string, inclusive: boolean): URI | undefined {
470+
const requestIndex = history.findIndex(s => s.requestId === requestId);
471+
if (requestIndex === -1) { return undefined; }
472+
const processedIndex = requestIndex + (inclusive ? 0 : 1);
473+
for (let i = processedIndex; i < history.length; i++) {
474+
const snapshot = history[i];
475+
for (const stop of snapshot.stops) {
476+
const entry = stop.entries.get(uri);
477+
if (entry) {
478+
return entry.snapshotUri;
479+
}
480+
}
481+
}
482+
return uri;
483+
}
421484
}
422485

423486
function stopProvidesNewData(origin: IChatEditingSessionStop, target: IChatEditingSessionStop) {

src/vs/workbench/contrib/chat/common/chatEditingService.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ export interface IChatEditingSession extends IDisposable {
143143
*/
144144
getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined): IObservable<IEditSessionEntryDiff | undefined> | undefined;
145145

146+
/**
147+
* Gets the document diff of a change made to a URI between one request to another one.
148+
* @returns The observable or undefined if there is no diff between the requests.
149+
*/
150+
getEntryDiffBetweenRequests(uri: URI, startRequestIs: string, stopRequestId: string): IObservable<IEditSessionEntryDiff | undefined>;
151+
146152
readonly canUndo: IObservable<boolean>;
147153
readonly canRedo: IObservable<boolean>;
148154
undoInteraction(): Promise<void>;

0 commit comments

Comments
 (0)