Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate IModifiedFileEntry into text and notebook #234388

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
*--------------------------------------------------------------------------------------------*/

import { RunOnceScheduler, timeout } from '../../../../../base/common/async.js';
import { VSBuffer } from '../../../../../base/common/buffer.js';
import { Emitter } from '../../../../../base/common/event.js';
import { StringSHA1 } from '../../../../../base/common/hash.js';
import { Disposable, IReference, toDisposable } from '../../../../../base/common/lifecycle.js';
import { IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js';
import { joinPath } from '../../../../../base/common/resources.js';
import { themeColorFromId } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js';
import { EditOperation, ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js';
import { OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js';
import { ISingleOffsetEdit, OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js';
import { IDocumentDiff, nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js';
import { TextEdit } from '../../../../../editor/common/languages.js';
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
Expand All @@ -23,18 +26,20 @@ import { IModelService } from '../../../../../editor/common/services/model.js';
import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js';
import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js';
import { localize } from '../../../../../nls.js';
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { editorSelectionBackground } from '../../../../../platform/theme/common/colorRegistry.js';
import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { SaveReason } from '../../../../common/editor.js';
import { IResolvedTextFileEditorModel, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js';
import { IChatAgentResult } from '../../common/chatAgents.js';
import { ChatEditKind, IModifiedFileEntry, WorkingSetEntryState } from '../../common/chatEditingService.js';
import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedTextFileEntry, ITextSnapshotEntry, ITextSnapshotEntryDTO, STORAGE_CONTENTS_FOLDER, WorkingSetEntryState } from '../../common/chatEditingService.js';
import { IChatService } from '../../common/chatService.js';
import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js';

export class ChatEditingModifiedFileEntry extends Disposable implements IModifiedFileEntry {

export class ChatEditingModifiedFileEntry extends Disposable implements IModifiedTextFileEntry {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plan is to create a new ChatEditingModifiedNotebookFileEntry class with custom handling for notebooks
That will then be responsible for handling notebook edits and the like (however quite a lot will be shared between the two)

public readonly kind = 'text';
public static readonly scheme = 'modified-file-entry';
private static lastEntryId = 0;
public readonly entryId = `${ChatEditingModifiedFileEntry.scheme}::${++ChatEditingModifiedFileEntry.lastEntryId}`;
Expand Down Expand Up @@ -129,6 +134,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
@IFileService private readonly _fileService: IFileService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
if (kind === ChatEditKind.Created) {
Expand Down Expand Up @@ -179,20 +185,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie
this._telemetryInfo = telemetryInfo;
}

createSnapshot(requestId: string | undefined): ISnapshotEntry {
createSnapshot(requestId: string | undefined): ITextSnapshotEntry {
this._isFirstEditAfterStartOrSnapshot = true;
return {
resource: this.modifiedURI,
languageId: this.modifiedModel.getLanguageId(),
snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(requestId, this.modifiedURI.path),
original: this.originalModel.getValue(),
current: this.modifiedModel.getValue(),
originalToCurrentEdit: this._edit,
state: this.state.get(),
telemetryInfo: this._telemetryInfo
};
}
restoreFromSnapshot(snapshot: ISnapshotEntry) {
return TextSnapshotEntry.create(this, this._telemetryInfo.sessionId, requestId, this._edit, this.instantiationService);
}

restoreFromSnapshot(snapshot: ITextSnapshotEntry) {
this._stateObs.set(snapshot.state, undefined);
this.docSnapshot.setValue(snapshot.original);
this._setDocValue(snapshot.current);
Expand Down Expand Up @@ -425,21 +423,104 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie
}
}

export interface IModifiedEntryTelemetryInfo {
readonly agentId: string | undefined;
readonly command: string | undefined;
readonly sessionId: string;
readonly requestId: string;
readonly result: IChatAgentResult | undefined;
}

export interface ISnapshotEntry {
readonly resource: URI;
readonly languageId: string;
readonly snapshotUri: URI;
readonly original: string;
readonly current: string;
readonly originalToCurrentEdit: OffsetEdit;
readonly state: WorkingSetEntryState;
telemetryInfo: IModifiedEntryTelemetryInfo;
export class TextSnapshotEntry implements ITextSnapshotEntry {
public readonly kind = 'text';
constructor(
public readonly languageId: string,
public readonly original: string,
public readonly current: string,
public readonly originalToCurrentEdit: OffsetEdit,
public readonly resource: URI,
public readonly snapshotUri: URI,
public readonly state: WorkingSetEntryState,
public readonly telemetryInfo: IModifiedEntryTelemetryInfo,
@IFileService private readonly _fileService: IFileService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
) {
}

public static create(entry: IModifiedTextFileEntry, chatSessionId: string,
requestId: string | undefined,
edit: OffsetEdit, instantiationService: IInstantiationService): TextSnapshotEntry {

return instantiationService.createInstance(TextSnapshotEntry,
entry.modifiedModel.getLanguageId(),
entry.originalModel.getValue(),
entry.modifiedModel.getValue(),
edit,
entry.modifiedURI,
ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(requestId, entry.modifiedURI.path),
entry.state.get(),
entry.telemetryInfo);
}

public static async deserialize(entry: ITextSnapshotEntryDTO, chatSessionId: string, instantiationService: IInstantiationService): Promise<TextSnapshotEntry> {
return instantiationService.invokeFunction(async accessor => {
const workspaceContextService = accessor.get(IWorkspaceContextService);
const environmentService = accessor.get(IEnvironmentService);
const fileService = accessor.get(IFileService);
const storageLocation = TextSnapshotEntry._getStorageLocation(chatSessionId, workspaceContextService, environmentService);

const [original, current] = await Promise.all([
TextSnapshotEntry.getFileContent(entry.originalHash, fileService, storageLocation),
TextSnapshotEntry.getFileContent(entry.currentHash, fileService, storageLocation)
]);

return instantiationService.createInstance(TextSnapshotEntry,
entry.languageId,
original,
current,
OffsetEdit.fromJson(entry.originalToCurrentEdit),
URI.parse(entry.resource),
URI.parse(entry.snapshotUri),
entry.state,
{ requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command, sessionId: chatSessionId, result: undefined }
);
});
}

async serialize(): Promise<ITextSnapshotEntryDTO> {
const fileContents = new Map<string, string>();
const serialized = {
kind: 'text',
resource: this.resource.toString(),
languageId: this.languageId,
originalHash: this.computeContentHash(this.original),
currentHash: this.computeContentHash(this.current),
originalToCurrentEdit: this.originalToCurrentEdit.edits.map(edit => ({ pos: edit.replaceRange.start, len: edit.replaceRange.length, txt: edit.newText } satisfies ISingleOffsetEdit)),
state: this.state,
snapshotUri: this.snapshotUri.toString(),
telemetryInfo: { requestId: this.telemetryInfo.requestId, agentId: this.telemetryInfo.agentId, command: this.telemetryInfo.command }
} satisfies ITextSnapshotEntryDTO;

const storageFolder = TextSnapshotEntry._getStorageLocation(this.telemetryInfo.sessionId, this._workspaceContextService, this._environmentService);
const contentsFolder = URI.joinPath(storageFolder, STORAGE_CONTENTS_FOLDER);

await Promise.all(Array.from(fileContents.entries()).map(async ([hash, content]) => {
const file = joinPath(contentsFolder, hash);
if (!(await this._fileService.exists(file))) {
await this._fileService.writeFile(joinPath(contentsFolder, hash), VSBuffer.fromString(content));
}
}));

return serialized;
}
private computeContentHash(content: string): string {
const shaComputer = new StringSHA1();
shaComputer.update(content);
return shaComputer.digest().substring(0, 7);
}
private static _getStorageLocation(chatSessionId: string,
_workspaceContextService: IWorkspaceContextService,
_environmentService: IEnvironmentService,
): URI {
const workspaceId = _workspaceContextService.getWorkspace().id;
return joinPath(_environmentService.workspaceStorageHome, workspaceId, 'chatEditingSessions', chatSessionId);
}
private static getFileContent(hash: string, fileService: IFileService, storageLocation: URI) {
return fileService.readFile(joinPath(storageLocation, STORAGE_CONTENTS_FOLDER, hash)).then(content => content.value.toString());
}
}

Loading
Loading