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

Serialize saveables to disk for "Save As" #13833

Merged
merged 1 commit into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 9 additions & 1 deletion packages/core/src/browser/saveable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Key } from './keyboard/keys';
import { AbstractDialog } from './dialogs';
import { nls } from '../common/nls';
import { DisposableCollection, isObject } from '../common';
import { BinaryBuffer } from '../common/buffer';

export type AutoSaveMode = 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange';

Expand Down Expand Up @@ -53,6 +54,10 @@ export interface Saveable {
* Applies the given snapshot to the dirty state.
*/
applySnapshot?(snapshot: object): void;
/**
* Serializes the full state of the saveable item to a binary buffer.
*/
serialize?(): Promise<BinaryBuffer>;
}

export interface SaveableSource {
Expand All @@ -79,6 +84,7 @@ export class DelegatingSaveable implements Saveable {
revert?(options?: Saveable.RevertOptions): Promise<void>;
createSnapshot?(): Saveable.Snapshot;
applySnapshot?(snapshot: object): void;
serialize?(): Promise<BinaryBuffer>;

protected _delegate?: Saveable;
protected toDispose = new DisposableCollection();
Expand All @@ -101,6 +107,7 @@ export class DelegatingSaveable implements Saveable {
this.revert = delegate.revert?.bind(delegate);
this.createSnapshot = delegate.createSnapshot?.bind(delegate);
this.applySnapshot = delegate.applySnapshot?.bind(delegate);
this.serialize = delegate.serialize?.bind(delegate);
}

}
Expand All @@ -115,7 +122,8 @@ export namespace Saveable {
}

/**
* A snapshot of a saveable item. Contains the full content of the saveable file in a string serializable format.
* A snapshot of a saveable item.
* Applying a snapshot of a saveable on another (of the same type) using the `applySnapshot` should yield the state of the original saveable.
*/
export type Snapshot = { value: string } | { read(): string | null };
export namespace Snapshot {
Expand Down
22 changes: 16 additions & 6 deletions packages/filesystem/src/browser/filesystem-saveable-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { SaveableService } from '@theia/core/lib/browser/saveable-service';
import URI from '@theia/core/lib/common/uri';
import { FileService } from './file-service';
import { FileDialogService } from './file-dialog';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';

@injectable()
export class FilesystemSaveableService extends SaveableService {
Expand All @@ -39,13 +40,13 @@ export class FilesystemSaveableService extends SaveableService {
/**
* This method ensures a few things about `widget`:
* - `widget.getResourceUri()` actually returns a URI.
* - `widget.saveable.createSnapshot` is defined.
* - `widget.saveable.createSnapshot` or `widget.saveable.serialize` is defined.
* - `widget.saveable.revert` is defined.
*/
override canSaveAs(widget: Widget | undefined): widget is Widget & SaveableSource & Navigatable {
return widget !== undefined
&& Saveable.isSource(widget)
&& typeof widget.saveable.createSnapshot === 'function'
&& (typeof widget.saveable.createSnapshot === 'function' || typeof widget.saveable.serialize === 'function')
&& typeof widget.saveable.revert === 'function'
&& Navigatable.is(widget)
&& widget.getResourceUri() !== undefined;
Expand Down Expand Up @@ -96,14 +97,23 @@ export class FilesystemSaveableService extends SaveableService {
*/
protected async saveSnapshot(sourceWidget: Widget & SaveableSource & Navigatable, target: URI, overwrite: boolean): Promise<void> {
const saveable = sourceWidget.saveable;
const snapshot = saveable.createSnapshot!();
const content = Saveable.Snapshot.read(snapshot) ?? '';
let buffer: BinaryBuffer;
if (saveable.serialize) {
buffer = await saveable.serialize();
} else if (saveable.createSnapshot) {
const snapshot = saveable.createSnapshot();
const content = Saveable.Snapshot.read(snapshot) ?? '';
buffer = BinaryBuffer.fromString(content);
} else {
throw new Error('Cannot save the widget as the saveable does not provide a snapshot or a serialize method.');
}

if (await this.fileService.exists(target)) {
// Do not fire the `onDidCreate` event as the file already exists.
await this.fileService.write(target, content);
await this.fileService.writeFile(target, buffer);
} else {
// Ensure to actually call `create` as that fires the `onDidCreate` event.
await this.fileService.create(target, content, { overwrite });
await this.fileService.createFile(target, buffer, { overwrite });
}
await saveable.revert!();
await open(this.openerService, target, { widgetOptions: { ref: sourceWidget, mode: 'tab-replace' } });
Expand Down
5 changes: 5 additions & 0 deletions packages/monaco/src/browser/monaco-editor-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { IModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/se
import { createTextBufferFactoryFromStream } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel';
import { editorGeneratedPreferenceProperties } from '@theia/editor/lib/browser/editor-generated-preference-schema';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';

export {
TextDocumentSaveReason
Expand Down Expand Up @@ -655,6 +656,10 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
this.model.setValue(value);
}

async serialize(): Promise<BinaryBuffer> {
return BinaryBuffer.fromString(this.model.getValue());
}

protected trace(loggable: Loggable): void {
if (this.logger) {
this.logger.debug((log: Log) =>
Expand Down
8 changes: 6 additions & 2 deletions packages/notebook/src/browser/view-model/notebook-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { inject, injectable, interfaces, postConstruct } from '@theia/core/share
import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import type { NotebookModelResolverService } from '../service/notebook-model-resolver-service';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';

export const NotebookModelFactory = Symbol('NotebookModelFactory');

Expand Down Expand Up @@ -176,8 +177,7 @@ export class NotebookModel implements Saveable, Disposable {
this.dirtyCells = [];
this.dirty = false;

const data = this.getData();
const serializedNotebook = await this.props.serializer.fromNotebook(data);
const serializedNotebook = await this.serialize();
this.fileService.writeFile(this.uri, serializedNotebook);

this.onDidSaveNotebookEmitter.fire();
Expand All @@ -189,6 +189,10 @@ export class NotebookModel implements Saveable, Disposable {
};
}

serialize(): Promise<BinaryBuffer> {
return this.props.serializer.fromNotebook(this.getData());
}

async applySnapshot(snapshot: Saveable.Snapshot): Promise<void> {
const rawData = Saveable.Snapshot.read(snapshot);
if (!rawData) {
Expand Down
Loading